Skip to content

Commit

Permalink
feat: support alist as a storage
Browse files Browse the repository at this point in the history
  • Loading branch information
simon-ding committed Nov 17, 2024
1 parent b136b91 commit 7d5ce8b
Show file tree
Hide file tree
Showing 14 changed files with 536 additions and 121 deletions.
2 changes: 1 addition & 1 deletion ent/migrate/schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ent/schema/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type Storage struct {
func (Storage) Fields() []ent.Field {
return []ent.Field{
field.String("name").Unique(),
field.Enum("implementation").Values("webdav", "local"),
field.Enum("implementation").Values("webdav", "local", "alist"),
field.String("tv_path").Optional(),
field.String("movie_path").Optional(),
field.String("settings").Optional(),
Expand Down
3 changes: 2 additions & 1 deletion ent/storage/storage.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

194 changes: 194 additions & 0 deletions pkg/alist/alist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package alist

import (
"bytes"
"encoding/json"
"io"
"net/http"
"strings"
"time"

"github.com/pkg/errors"
)

type Resposne[T any] struct {
Code int `json:"code"`
Message string `json:"message"`
Data T `json:"data"`
}

type Config struct {
Username string
Password string
URL string
}

func New(cfg *Config) *Client {
cfg.URL = strings.Trim(cfg.URL, "/")
return &Client{
cfg: cfg,
http: http.DefaultClient,
}
}

type Client struct {
cfg *Config
http *http.Client
token string
}

func (c *Client) Login() (string, error) {
p := map[string]string{
"username": c.cfg.Username,
"password": c.cfg.Password,
}
data, _ := json.Marshal(p)
resp, err := c.http.Post(c.cfg.URL+loginUrl, "application/json", bytes.NewBuffer(data))
if err != nil {
return "", errors.Wrap(err, "login")
}
defer resp.Body.Close()
d1, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.Wrap(err, "read body")
}
var rp Resposne[map[string]string]

err = json.Unmarshal(d1, &rp)
if err != nil {
return "", errors.Wrap(err, "json")
}
if rp.Code != 200 {
return "", errors.Errorf("alist error: code %d, %s", rp.Code, rp.Message)
}
c.token = rp.Data["token"]
return c.token, nil
}

type LsInfo struct {
Content []struct {
Name string `json:"name"`
Size int `json:"size"`
IsDir bool `json:"is_dir"`
Modified time.Time `json:"modified"`
Created time.Time `json:"created"`
Sign string `json:"sign"`
Thumb string `json:"thumb"`
Type int `json:"type"`
Hashinfo string `json:"hashinfo"`
HashInfo any `json:"hash_info"`
} `json:"content"`
Total int `json:"total"`
Readme string `json:"readme"`
Header string `json:"header"`
Write bool `json:"write"`
Provider string `json:"provider"`
}

func (c *Client) Ls(dir string) (*LsInfo, error) {
in := map[string]string{
"path": dir,
}

resp, err := c.post(c.cfg.URL+lsUrl, in)
if err != nil {
return nil, errors.Wrap(err, "http")
}

var out Resposne[LsInfo]
err = json.Unmarshal(resp, &out)
if err != nil {
return nil, err
}
if out.Code != 200 {
return nil, errors.Errorf("alist error: code %d, %s", out.Code, out.Message)
}
return &out.Data, nil
}

func (c *Client) Mkdir(dir string) error {
in := map[string]string{
"path": dir,
}
resp, err := c.post(c.cfg.URL+mkdirUrl, in)
if err != nil {
return errors.Wrap(err, "http")
}
var out Resposne[any]
err = json.Unmarshal(resp, &out)
if err != nil {
return err
}
if out.Code != 200 {
return errors.Errorf("alist error: code %d, %s", out.Code, out.Message)
}
return nil
}

func (c *Client) post(url string, body interface{}) ([]byte, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
if err != nil {
return nil, errors.Wrap(err, "new request")
}

req.Header.Add("Authorization", c.token)
req.Header.Set("Content-Type", "application/json")

resp, err := c.http.Do(req)
if err != nil {
return nil, errors.Wrap(err, "http")
}
defer resp.Body.Close()
d1, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read body")
}
return d1, nil
}

type UploadStreamResponse 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"`
}

func (c *Client) UploadStream(reader io.Reader, toDir string) (*UploadStreamResponse, error) {
req, err := http.NewRequest(http.MethodPost, c.cfg.URL+streamUploadUrl, reader)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "{{alist_token}}")
req.Header.Add("File-Path", toDir)
req.Header.Add("As-Task", "true")
req.Header.Add("Content-Length", "")
req.Header.Add("Content-Type", "application/octet-stream")
res, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
d1, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}

var out Resposne[UploadStreamResponse]
err = json.Unmarshal(d1, &out)
if err != nil {
return nil, err
}
if out.Code != 200 {
return nil, errors.Errorf("alist error: code %d, %s", out.Code, out.Message)
}

return &out.Data, nil
}
30 changes: 30 additions & 0 deletions pkg/alist/alist_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package alist

import (
"polaris/log"
"testing"
)

func TestLogin(t *testing.T) {
c := New(&Config{
URL: "http://10.0.0.8:5244/",
Username: "",
Password: "",
})
cre, err := c.Login()
if err != nil {
log.Errorf("login fail: %v", err)
t.Fail()
} else {
log.Errorf("login success: %s", cre)
}
info, err := c.Ls("/aliyun")
if err != nil {
log.Errorf("ls fail: %v", err)
t.Fail()
} else {
log.Infof("ls results: %+v", info)
}
err = c.Mkdir("/aliyun/test1")
log.Errorf("mkdir: %v", err)
}
8 changes: 8 additions & 0 deletions pkg/alist/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package alist

const (
loginUrl = "/api/auth/login"
lsUrl = "/api/fs/list"
mkdirUrl = "/api/fs/mkdir"
streamUploadUrl = "/api/fs/put"
)
72 changes: 72 additions & 0 deletions pkg/storage/alist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package storage

import (
"io"
"io/fs"
"os"
"path/filepath"
"polaris/pkg/alist"

"github.com/gabriel-vasile/mimetype"
)

func NewAlist(cfg *alist.Config, dir string) (*Alist, error) {
cl := alist.New(cfg)
_, err := cl.Login()
if err != nil {
return nil, err
}
return &Alist{baseDir: dir, cfg: cfg, client: cl}, nil
}

type Alist struct {
baseDir string
cfg *alist.Config
client *alist.Client
progresser func() float64
}

func (a *Alist) Move(src, dest string) error {
if err := a.Copy(src, dest); err != nil {
return err
}
return os.RemoveAll(src)
}

func (a *Alist) Copy(src, dest string) error {
b, err := NewBase(src)
if err != nil {
return err
}
a.progresser = b.Progress

uploadFunc := func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mimeType *mimetype.MIME) error {
_, err := a.client.UploadStream(srcReader, destPath)
return err
}
mkdirFunc := func(dir string) error {
return a.client.Mkdir(dir)
}

baseDest := filepath.Join(a.baseDir, dest)
return b.Upload(baseDest, false, false, false, uploadFunc, mkdirFunc)
}

func (a *Alist) ReadDir(dir string) ([]fs.FileInfo, error) {
return nil, nil
}

func (a *Alist) ReadFile(s string) ([]byte, error) {
return nil, nil
}

func (a *Alist) WriteFile(s string, bytes []byte) error {
return nil
}

func (a *Alist) UploadProgress() float64 {
if a.progresser == nil {
return 0
}
return a.progresser()
}
Loading

0 comments on commit 7d5ce8b

Please sign in to comment.