mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-05-18 05:17:37 +08:00
feat!: (WIP) decouple storage, users, and configuration files to support multiple users
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
package alist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
type Alist struct {
|
||||
@@ -21,154 +21,72 @@ type Alist struct {
|
||||
token string
|
||||
baseURL string
|
||||
loginInfo *loginRequest
|
||||
config config.AlistConfig
|
||||
}
|
||||
|
||||
var (
|
||||
ErrAlistLoginFailed = errors.New("failed to login to Alist")
|
||||
)
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type loginResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type meResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type putResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Task struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
State int `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
Error string `json:"error"`
|
||||
} `json:"task"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func (a *Alist) getToken() error {
|
||||
loginBody, err := json.Marshal(a.loginInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal login request: %w", err)
|
||||
func (a *Alist) Init(model types.StorageModel) error {
|
||||
var alistConfig config.AlistConfig
|
||||
if err := json.Unmarshal([]byte(model.Config), &alistConfig); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal alist config: %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 {
|
||||
time.Sleep(time.Duration(config.Cfg.Storage.Alist.TokenExp) * time.Second)
|
||||
if err := a.getToken(); err != nil {
|
||||
logger.L.Errorf("Failed to refresh jwt token: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.L.Info("Refreshed Alist jwt token")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Alist) Init() {
|
||||
a.baseURL = config.Cfg.Storage.Alist.URL
|
||||
a.client = &http.Client{
|
||||
Timeout: 12 * time.Hour,
|
||||
Transport: &http.Transport{
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
if config.Cfg.Storage.Alist.Token != "" {
|
||||
a.token = config.Cfg.Storage.Alist.Token
|
||||
a.config = alistConfig
|
||||
a.baseURL = alistConfig.URL
|
||||
a.client = getHttpClient()
|
||||
if alistConfig.Token != "" {
|
||||
a.token = alistConfig.Token
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"/api/me", nil)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to create request: %v", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", a.token)
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to send request: %v", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.L.Fatalf("Failed to get alist user info: %s", resp.Status)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to read response body: %v", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
var meResp meResponse
|
||||
if err := json.Unmarshal(body, &meResp); err != nil {
|
||||
logger.L.Fatalf("Failed to unmarshal me response: %v", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
if meResp.Code != http.StatusOK {
|
||||
logger.L.Fatalf("Failed to get alist user info: %s", meResp.Message)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
logger.L.Debugf("Logged in Alist as %s", meResp.Data.Username)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
a.loginInfo = &loginRequest{
|
||||
Username: config.Cfg.Storage.Alist.Username,
|
||||
Password: config.Cfg.Storage.Alist.Password,
|
||||
Username: alistConfig.Username,
|
||||
Password: alistConfig.Password,
|
||||
}
|
||||
|
||||
if err := a.getToken(); err != nil {
|
||||
logger.L.Fatalf("Failed to login to Alist: %v", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
logger.L.Debug("Logged in to Alist")
|
||||
|
||||
go a.refreshToken()
|
||||
go a.refreshToken(alistConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Alist) Type() types.StorageType {
|
||||
return types.StorageTypeAlist
|
||||
}
|
||||
|
||||
func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
@@ -219,3 +137,7 @@ func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Alist) JoinStoragePath(task types.Task) string {
|
||||
return path.Join(a.config.BasePath, task.StoragePath)
|
||||
}
|
||||
|
||||
60
storage/alist/token.go
Normal file
60
storage/alist/token.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package alist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
)
|
||||
|
||||
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(cfg config.AlistConfig) {
|
||||
for {
|
||||
time.Sleep(time.Duration(cfg.TokenExp) * time.Second)
|
||||
if err := a.getToken(); err != nil {
|
||||
logger.L.Errorf("Failed to refresh jwt token: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.L.Info("Refreshed Alist jwt token")
|
||||
}
|
||||
}
|
||||
44
storage/alist/types.go
Normal file
44
storage/alist/types.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package alist
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrAlistLoginFailed = errors.New("failed to login to Alist")
|
||||
)
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type loginResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type meResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type putResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Task struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
State int `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
Error string `json:"error"`
|
||||
} `json:"task"`
|
||||
} `json:"data"`
|
||||
}
|
||||
23
storage/alist/utils.go
Normal file
23
storage/alist/utils.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package alist
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
httpClient *http.Client
|
||||
)
|
||||
|
||||
func getHttpClient() *http.Client {
|
||||
if httpClient != nil {
|
||||
return httpClient
|
||||
}
|
||||
httpClient = &http.Client{
|
||||
Timeout: 12 * time.Hour,
|
||||
Transport: &http.Transport{
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
return httpClient
|
||||
}
|
||||
@@ -2,22 +2,35 @@ package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/duke-git/lancet/v2/fileutil"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
type Local struct{}
|
||||
type Local struct {
|
||||
config config.LocalConfig
|
||||
}
|
||||
|
||||
func (l *Local) Init() {
|
||||
err := os.MkdirAll(config.Cfg.Storage.Local.BasePath, os.ModePerm)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to create local storage directory: %s", err)
|
||||
os.Exit(1)
|
||||
func (l *Local) Init(model types.StorageModel) error {
|
||||
var localConfig config.LocalConfig
|
||||
if err := json.Unmarshal([]byte(model.Config), &localConfig); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal local config: %w", err)
|
||||
}
|
||||
l.config = localConfig
|
||||
err := os.MkdirAll(localConfig.BasePath, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create local storage directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Local) Type() types.StorageType {
|
||||
return types.StorageTypeLocal
|
||||
}
|
||||
|
||||
func (l *Local) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
@@ -30,3 +43,7 @@ func (l *Local) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
}
|
||||
return fileutil.CopyFile(filePath, storagePath)
|
||||
}
|
||||
|
||||
func (l *Local) JoinStoragePath(task types.Task) string {
|
||||
return filepath.Join(l.config.BasePath, task.StoragePath)
|
||||
}
|
||||
|
||||
@@ -3,13 +3,8 @@ package storage
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/dao"
|
||||
"github.com/krau/SaveAny-Bot/storage/alist"
|
||||
"github.com/krau/SaveAny-Bot/storage/local"
|
||||
"github.com/krau/SaveAny-Bot/storage/webdav"
|
||||
@@ -17,68 +12,72 @@ import (
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
Init()
|
||||
Init(model types.StorageModel) error
|
||||
Type() types.StorageType
|
||||
JoinStoragePath(task types.Task) string
|
||||
Save(cttx context.Context, localFilePath, storagePath string) error
|
||||
}
|
||||
|
||||
var Storages = make(map[types.StorageType]Storage)
|
||||
var StorageKeys = make([]types.StorageType, 0)
|
||||
var (
|
||||
ErrInvalidStorageID = errors.New("invalid storage ID")
|
||||
)
|
||||
|
||||
func Init() {
|
||||
logger.L.Debug("Initializing storage...")
|
||||
if config.Cfg.Storage.Alist.Enable {
|
||||
Storages[types.Alist] = new(alist.Alist)
|
||||
Storages[types.Alist].Init()
|
||||
}
|
||||
if config.Cfg.Storage.Local.Enable {
|
||||
Storages[types.Local] = new(local.Local)
|
||||
Storages[types.Local].Init()
|
||||
}
|
||||
if config.Cfg.Storage.Webdav.Enable {
|
||||
Storages[types.Webdav] = new(webdav.Webdav)
|
||||
Storages[types.Webdav].Init()
|
||||
}
|
||||
var Storages = make(map[uint]Storage)
|
||||
|
||||
for k := range Storages {
|
||||
StorageKeys = append(StorageKeys, k)
|
||||
// LoadExistingStorages loads existing storages from the database, and initializes them
|
||||
//
|
||||
// Should only be called at startup
|
||||
func LoadExistingStorages() error {
|
||||
storageModels, err := dao.GetActiveStorages()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slice.Sort(StorageKeys)
|
||||
|
||||
logger.L.Debug("Storage initialized")
|
||||
}
|
||||
|
||||
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 {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if storageType != types.StorageAll {
|
||||
return Storages[storageType].Save(ctx, filePath, storagePath)
|
||||
}
|
||||
errs := make([]error, 0)
|
||||
var wg sync.WaitGroup
|
||||
for _, storage := range Storages {
|
||||
wg.Add(1)
|
||||
go func(storage Storage) {
|
||||
defer wg.Done()
|
||||
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)
|
||||
}
|
||||
}(storage)
|
||||
}
|
||||
wg.Wait()
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
for _, storageModel := range storageModels {
|
||||
storage, err := NewStorage(storageModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Storages[storageModel.ID] = storage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get storage from model, if it exists, otherwise create and init a new storage
|
||||
func GetStorageFromModel(model types.StorageModel) (Storage, error) {
|
||||
if model.ID == 0 {
|
||||
return nil, ErrInvalidStorageID
|
||||
}
|
||||
if storage, ok := Storages[model.ID]; ok {
|
||||
return storage, nil
|
||||
}
|
||||
storage, err := NewStorage(model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
Storages[model.ID] = storage
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
func NewStorage(storageModel types.StorageModel) (Storage, error) {
|
||||
switch storageModel.Type {
|
||||
case string(types.StorageTypeAlist):
|
||||
alistStorage := new(alist.Alist)
|
||||
if err := alistStorage.Init(storageModel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return alistStorage, nil
|
||||
case string(types.StorageTypeLocal):
|
||||
localStorage := new(local.Local)
|
||||
if err := localStorage.Init(storageModel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return localStorage, nil
|
||||
case string(types.StorageTypeWebdav):
|
||||
webdavStorage := new(webdav.Webdav)
|
||||
if err := webdavStorage.Init(storageModel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return webdavStorage, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -2,29 +2,42 @@ package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
"github.com/studio-b12/gowebdav"
|
||||
)
|
||||
|
||||
type Webdav struct{}
|
||||
type Webdav struct {
|
||||
config config.WebdavConfig
|
||||
}
|
||||
|
||||
var (
|
||||
Client *gowebdav.Client
|
||||
)
|
||||
|
||||
func (w *Webdav) Init() {
|
||||
webdavConfig := config.Cfg.Storage.Webdav
|
||||
func (w *Webdav) Init(model types.StorageModel) error {
|
||||
var webdavConfig config.WebdavConfig
|
||||
if err := json.Unmarshal([]byte(model.Config), &webdavConfig); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal webdav config: %w", err)
|
||||
}
|
||||
w.config = webdavConfig
|
||||
Client = gowebdav.NewClient(webdavConfig.URL, webdavConfig.Username, webdavConfig.Password)
|
||||
if err := Client.Connect(); err != nil {
|
||||
logger.L.Fatalf("Failed to connect to webdav server: %v", err)
|
||||
os.Exit(1)
|
||||
return fmt.Errorf("failed to connect to webdav server: %w", err)
|
||||
}
|
||||
Client.SetTimeout(24 * time.Hour)
|
||||
Client.SetTimeout(12 * time.Hour)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Webdav) Type() types.StorageType {
|
||||
return types.StorageTypeWebdav
|
||||
}
|
||||
|
||||
func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
@@ -45,3 +58,7 @@ func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Webdav) JoinStoragePath(task types.Task) string {
|
||||
return path.Join(w.config.BasePath, task.StoragePath)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user