From 0cbc7ebc92b2d64299ab6014be4c3feffeb1c967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E7=9F=B3?= Date: Tue, 11 Nov 2025 20:25:26 +0800 Subject: [PATCH] feat(driver): Added support for Gitee driver (#9368) * feat(driver): Added support for Gitee driver - Implemented core driver functions including initialization, file listing, and file linking - Added Gitee-specific API interaction and object mapping - Registered Gitee driver in the driver registry * feat(driver): Added cookie-based authentication support for Gitee driver - Extended request handling to include `Cookie` header if provided - Updated metadata to include `cookie` field with appropriate documentation - Adjusted file link generation to propagate `Cookie` headers in requests --- drivers/all.go | 1 + drivers/gitee/driver.go | 224 ++++++++++++++++++++++++++++++++++++++++ drivers/gitee/meta.go | 29 ++++++ drivers/gitee/types.go | 60 +++++++++++ drivers/gitee/util.go | 44 ++++++++ 5 files changed, 358 insertions(+) create mode 100644 drivers/gitee/driver.go create mode 100644 drivers/gitee/meta.go create mode 100644 drivers/gitee/types.go create mode 100644 drivers/gitee/util.go diff --git a/drivers/all.go b/drivers/all.go index efeb6f77..3eb7e813 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -31,6 +31,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/dropbox" _ "github.com/alist-org/alist/v3/drivers/febbox" _ "github.com/alist-org/alist/v3/drivers/ftp" + _ "github.com/alist-org/alist/v3/drivers/gitee" _ "github.com/alist-org/alist/v3/drivers/github" _ "github.com/alist-org/alist/v3/drivers/github_releases" _ "github.com/alist-org/alist/v3/drivers/gofile" diff --git a/drivers/gitee/driver.go b/drivers/gitee/driver.go new file mode 100644 index 00000000..78a40094 --- /dev/null +++ b/drivers/gitee/driver.go @@ -0,0 +1,224 @@ +package gitee + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + stdpath "path" + "strings" + + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/errs" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/pkg/utils" + "github.com/go-resty/resty/v2" +) + +type Gitee struct { + model.Storage + Addition + client *resty.Client +} + +func (d *Gitee) Config() driver.Config { + return config +} + +func (d *Gitee) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *Gitee) Init(ctx context.Context) error { + d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath) + d.Endpoint = strings.TrimSpace(d.Endpoint) + if d.Endpoint == "" { + d.Endpoint = "https://gitee.com/api/v5" + } + d.Endpoint = strings.TrimSuffix(d.Endpoint, "/") + d.Owner = strings.TrimSpace(d.Owner) + d.Repo = strings.TrimSpace(d.Repo) + d.Token = strings.TrimSpace(d.Token) + d.DownloadProxy = strings.TrimSpace(d.DownloadProxy) + if d.Owner == "" || d.Repo == "" { + return errors.New("owner and repo are required") + } + d.client = base.NewRestyClient(). + SetBaseURL(d.Endpoint). + SetHeader("Accept", "application/json") + repo, err := d.getRepo() + if err != nil { + return err + } + d.Ref = strings.TrimSpace(d.Ref) + if d.Ref == "" { + d.Ref = repo.DefaultBranch + } + return nil +} + +func (d *Gitee) Drop(ctx context.Context) error { + return nil +} + +func (d *Gitee) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + relPath := d.relativePath(dir.GetPath()) + contents, err := d.listContents(relPath) + if err != nil { + return nil, err + } + objs := make([]model.Obj, 0, len(contents)) + for i := range contents { + objs = append(objs, contents[i].toModelObj()) + } + return objs, nil +} + +func (d *Gitee) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + var downloadURL string + if obj, ok := file.(*Object); ok { + downloadURL = obj.DownloadURL + if downloadURL == "" { + relPath := d.relativePath(file.GetPath()) + content, err := d.getContent(relPath) + if err != nil { + return nil, err + } + if content.DownloadURL == "" { + return nil, errors.New("empty download url") + } + obj.DownloadURL = content.DownloadURL + downloadURL = content.DownloadURL + } + } else { + relPath := d.relativePath(file.GetPath()) + content, err := d.getContent(relPath) + if err != nil { + return nil, err + } + if content.DownloadURL == "" { + return nil, errors.New("empty download url") + } + downloadURL = content.DownloadURL + } + url := d.applyProxy(downloadURL) + return &model.Link{ + URL: url, + Header: http.Header{ + "Cookie": {d.Cookie}, + }, + }, nil +} + +func (d *Gitee) newRequest() *resty.Request { + req := d.client.R() + if d.Token != "" { + req.SetQueryParam("access_token", d.Token) + } + if d.Ref != "" { + req.SetQueryParam("ref", d.Ref) + } + return req +} + +func (d *Gitee) apiPath(path string) string { + escapedOwner := url.PathEscape(d.Owner) + escapedRepo := url.PathEscape(d.Repo) + if path == "" { + return fmt.Sprintf("/repos/%s/%s/contents", escapedOwner, escapedRepo) + } + return fmt.Sprintf("/repos/%s/%s/contents/%s", escapedOwner, escapedRepo, encodePath(path)) +} + +func (d *Gitee) listContents(path string) ([]Content, error) { + res, err := d.newRequest().Get(d.apiPath(path)) + if err != nil { + return nil, err + } + if res.IsError() { + return nil, toErr(res) + } + var contents []Content + if err := utils.Json.Unmarshal(res.Body(), &contents); err != nil { + var single Content + if err2 := utils.Json.Unmarshal(res.Body(), &single); err2 == nil && single.Type != "" { + if single.Type != "dir" { + return nil, errs.NotFolder + } + return []Content{}, nil + } + return nil, err + } + for i := range contents { + contents[i].Path = joinPath(path, contents[i].Name) + } + return contents, nil +} + +func (d *Gitee) getContent(path string) (*Content, error) { + res, err := d.newRequest().Get(d.apiPath(path)) + if err != nil { + return nil, err + } + if res.IsError() { + return nil, toErr(res) + } + var content Content + if err := utils.Json.Unmarshal(res.Body(), &content); err != nil { + return nil, err + } + if content.Type == "" { + return nil, errors.New("invalid response") + } + if content.Path == "" { + content.Path = path + } + return &content, nil +} + +func (d *Gitee) relativePath(full string) string { + full = utils.FixAndCleanPath(full) + root := utils.FixAndCleanPath(d.RootFolderPath) + if root == "/" { + return strings.TrimPrefix(full, "/") + } + if utils.PathEqual(full, root) { + return "" + } + prefix := utils.PathAddSeparatorSuffix(root) + if strings.HasPrefix(full, prefix) { + return strings.TrimPrefix(full, prefix) + } + return strings.TrimPrefix(full, "/") +} + +func (d *Gitee) applyProxy(raw string) string { + if raw == "" || d.DownloadProxy == "" { + return raw + } + proxy := d.DownloadProxy + if !strings.HasSuffix(proxy, "/") { + proxy += "/" + } + return proxy + strings.TrimLeft(raw, "/") +} + +func encodePath(p string) string { + if p == "" { + return "" + } + parts := strings.Split(p, "/") + for i, part := range parts { + parts[i] = url.PathEscape(part) + } + return strings.Join(parts, "/") +} + +func joinPath(base, name string) string { + if base == "" { + return name + } + return strings.TrimPrefix(stdpath.Join(base, name), "./") +} diff --git a/drivers/gitee/meta.go b/drivers/gitee/meta.go new file mode 100644 index 00000000..2f926d63 --- /dev/null +++ b/drivers/gitee/meta.go @@ -0,0 +1,29 @@ +package gitee + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + driver.RootPath + Endpoint string `json:"endpoint" type:"string" help:"Gitee API endpoint, default https://gitee.com/api/v5"` + Token string `json:"token" type:"string"` + Owner string `json:"owner" type:"string" required:"true"` + Repo string `json:"repo" type:"string" required:"true"` + Ref string `json:"ref" type:"string" help:"Branch, tag or commit SHA, defaults to repository default branch"` + DownloadProxy string `json:"download_proxy" type:"string" help:"Prefix added before download URLs, e.g. https://mirror.example.com/"` + Cookie string `json:"cookie" type:"string" help:"Cookie returned from user info request"` +} + +var config = driver.Config{ + Name: "Gitee", + LocalSort: true, + DefaultRoot: "/", +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &Gitee{} + }) +} diff --git a/drivers/gitee/types.go b/drivers/gitee/types.go new file mode 100644 index 00000000..c10536a5 --- /dev/null +++ b/drivers/gitee/types.go @@ -0,0 +1,60 @@ +package gitee + +import ( + "time" + + "github.com/alist-org/alist/v3/internal/model" +) + +type Links struct { + Self string `json:"self"` + Html string `json:"html"` +} + +type Content struct { + Type string `json:"type"` + Size *int64 `json:"size"` + Name string `json:"name"` + Path string `json:"path"` + Sha string `json:"sha"` + URL string `json:"url"` + HtmlURL string `json:"html_url"` + DownloadURL string `json:"download_url"` + Links Links `json:"_links"` +} + +func (c Content) toModelObj() model.Obj { + size := int64(0) + if c.Size != nil { + size = *c.Size + } + return &Object{ + Object: model.Object{ + ID: c.Path, + Name: c.Name, + Size: size, + Modified: time.Unix(0, 0), + IsFolder: c.Type == "dir", + }, + DownloadURL: c.DownloadURL, + HtmlURL: c.HtmlURL, + } +} + +type Object struct { + model.Object + DownloadURL string + HtmlURL string +} + +func (o *Object) URL() string { + return o.DownloadURL +} + +type Repo struct { + DefaultBranch string `json:"default_branch"` +} + +type ErrResp struct { + Message string `json:"message"` +} diff --git a/drivers/gitee/util.go b/drivers/gitee/util.go new file mode 100644 index 00000000..fbef972a --- /dev/null +++ b/drivers/gitee/util.go @@ -0,0 +1,44 @@ +package gitee + +import ( + "fmt" + "net/url" + + "github.com/alist-org/alist/v3/pkg/utils" + "github.com/go-resty/resty/v2" +) + +func (d *Gitee) getRepo() (*Repo, error) { + req := d.client.R() + if d.Token != "" { + req.SetQueryParam("access_token", d.Token) + } + if d.Cookie != "" { + req.SetHeader("Cookie", d.Cookie) + } + escapedOwner := url.PathEscape(d.Owner) + escapedRepo := url.PathEscape(d.Repo) + res, err := req.Get(fmt.Sprintf("/repos/%s/%s", escapedOwner, escapedRepo)) + if err != nil { + return nil, err + } + if res.IsError() { + return nil, toErr(res) + } + var repo Repo + if err := utils.Json.Unmarshal(res.Body(), &repo); err != nil { + return nil, err + } + if repo.DefaultBranch == "" { + return nil, fmt.Errorf("failed to fetch default branch") + } + return &repo, nil +} + +func toErr(res *resty.Response) error { + var errMsg ErrResp + if err := utils.Json.Unmarshal(res.Body(), &errMsg); err == nil && errMsg.Message != "" { + return fmt.Errorf("%s: %s", res.Status(), errMsg.Message) + } + return fmt.Errorf(res.Status()) +}