Skip to content

Commit 0cbc7eb

Browse files
authored
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
1 parent b4d9beb commit 0cbc7eb

File tree

5 files changed

+358
-0
lines changed

5 files changed

+358
-0
lines changed

drivers/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
_ "github.com/alist-org/alist/v3/drivers/dropbox"
3232
_ "github.com/alist-org/alist/v3/drivers/febbox"
3333
_ "github.com/alist-org/alist/v3/drivers/ftp"
34+
_ "github.com/alist-org/alist/v3/drivers/gitee"
3435
_ "github.com/alist-org/alist/v3/drivers/github"
3536
_ "github.com/alist-org/alist/v3/drivers/github_releases"
3637
_ "github.com/alist-org/alist/v3/drivers/gofile"

drivers/gitee/driver.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package gitee
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"net/url"
9+
stdpath "path"
10+
"strings"
11+
12+
"github.com/alist-org/alist/v3/drivers/base"
13+
"github.com/alist-org/alist/v3/internal/driver"
14+
"github.com/alist-org/alist/v3/internal/errs"
15+
"github.com/alist-org/alist/v3/internal/model"
16+
"github.com/alist-org/alist/v3/pkg/utils"
17+
"github.com/go-resty/resty/v2"
18+
)
19+
20+
type Gitee struct {
21+
model.Storage
22+
Addition
23+
client *resty.Client
24+
}
25+
26+
func (d *Gitee) Config() driver.Config {
27+
return config
28+
}
29+
30+
func (d *Gitee) GetAddition() driver.Additional {
31+
return &d.Addition
32+
}
33+
34+
func (d *Gitee) Init(ctx context.Context) error {
35+
d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)
36+
d.Endpoint = strings.TrimSpace(d.Endpoint)
37+
if d.Endpoint == "" {
38+
d.Endpoint = "https://gitee.com/api/v5"
39+
}
40+
d.Endpoint = strings.TrimSuffix(d.Endpoint, "/")
41+
d.Owner = strings.TrimSpace(d.Owner)
42+
d.Repo = strings.TrimSpace(d.Repo)
43+
d.Token = strings.TrimSpace(d.Token)
44+
d.DownloadProxy = strings.TrimSpace(d.DownloadProxy)
45+
if d.Owner == "" || d.Repo == "" {
46+
return errors.New("owner and repo are required")
47+
}
48+
d.client = base.NewRestyClient().
49+
SetBaseURL(d.Endpoint).
50+
SetHeader("Accept", "application/json")
51+
repo, err := d.getRepo()
52+
if err != nil {
53+
return err
54+
}
55+
d.Ref = strings.TrimSpace(d.Ref)
56+
if d.Ref == "" {
57+
d.Ref = repo.DefaultBranch
58+
}
59+
return nil
60+
}
61+
62+
func (d *Gitee) Drop(ctx context.Context) error {
63+
return nil
64+
}
65+
66+
func (d *Gitee) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
67+
relPath := d.relativePath(dir.GetPath())
68+
contents, err := d.listContents(relPath)
69+
if err != nil {
70+
return nil, err
71+
}
72+
objs := make([]model.Obj, 0, len(contents))
73+
for i := range contents {
74+
objs = append(objs, contents[i].toModelObj())
75+
}
76+
return objs, nil
77+
}
78+
79+
func (d *Gitee) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
80+
var downloadURL string
81+
if obj, ok := file.(*Object); ok {
82+
downloadURL = obj.DownloadURL
83+
if downloadURL == "" {
84+
relPath := d.relativePath(file.GetPath())
85+
content, err := d.getContent(relPath)
86+
if err != nil {
87+
return nil, err
88+
}
89+
if content.DownloadURL == "" {
90+
return nil, errors.New("empty download url")
91+
}
92+
obj.DownloadURL = content.DownloadURL
93+
downloadURL = content.DownloadURL
94+
}
95+
} else {
96+
relPath := d.relativePath(file.GetPath())
97+
content, err := d.getContent(relPath)
98+
if err != nil {
99+
return nil, err
100+
}
101+
if content.DownloadURL == "" {
102+
return nil, errors.New("empty download url")
103+
}
104+
downloadURL = content.DownloadURL
105+
}
106+
url := d.applyProxy(downloadURL)
107+
return &model.Link{
108+
URL: url,
109+
Header: http.Header{
110+
"Cookie": {d.Cookie},
111+
},
112+
}, nil
113+
}
114+
115+
func (d *Gitee) newRequest() *resty.Request {
116+
req := d.client.R()
117+
if d.Token != "" {
118+
req.SetQueryParam("access_token", d.Token)
119+
}
120+
if d.Ref != "" {
121+
req.SetQueryParam("ref", d.Ref)
122+
}
123+
return req
124+
}
125+
126+
func (d *Gitee) apiPath(path string) string {
127+
escapedOwner := url.PathEscape(d.Owner)
128+
escapedRepo := url.PathEscape(d.Repo)
129+
if path == "" {
130+
return fmt.Sprintf("/repos/%s/%s/contents", escapedOwner, escapedRepo)
131+
}
132+
return fmt.Sprintf("/repos/%s/%s/contents/%s", escapedOwner, escapedRepo, encodePath(path))
133+
}
134+
135+
func (d *Gitee) listContents(path string) ([]Content, error) {
136+
res, err := d.newRequest().Get(d.apiPath(path))
137+
if err != nil {
138+
return nil, err
139+
}
140+
if res.IsError() {
141+
return nil, toErr(res)
142+
}
143+
var contents []Content
144+
if err := utils.Json.Unmarshal(res.Body(), &contents); err != nil {
145+
var single Content
146+
if err2 := utils.Json.Unmarshal(res.Body(), &single); err2 == nil && single.Type != "" {
147+
if single.Type != "dir" {
148+
return nil, errs.NotFolder
149+
}
150+
return []Content{}, nil
151+
}
152+
return nil, err
153+
}
154+
for i := range contents {
155+
contents[i].Path = joinPath(path, contents[i].Name)
156+
}
157+
return contents, nil
158+
}
159+
160+
func (d *Gitee) getContent(path string) (*Content, error) {
161+
res, err := d.newRequest().Get(d.apiPath(path))
162+
if err != nil {
163+
return nil, err
164+
}
165+
if res.IsError() {
166+
return nil, toErr(res)
167+
}
168+
var content Content
169+
if err := utils.Json.Unmarshal(res.Body(), &content); err != nil {
170+
return nil, err
171+
}
172+
if content.Type == "" {
173+
return nil, errors.New("invalid response")
174+
}
175+
if content.Path == "" {
176+
content.Path = path
177+
}
178+
return &content, nil
179+
}
180+
181+
func (d *Gitee) relativePath(full string) string {
182+
full = utils.FixAndCleanPath(full)
183+
root := utils.FixAndCleanPath(d.RootFolderPath)
184+
if root == "/" {
185+
return strings.TrimPrefix(full, "/")
186+
}
187+
if utils.PathEqual(full, root) {
188+
return ""
189+
}
190+
prefix := utils.PathAddSeparatorSuffix(root)
191+
if strings.HasPrefix(full, prefix) {
192+
return strings.TrimPrefix(full, prefix)
193+
}
194+
return strings.TrimPrefix(full, "/")
195+
}
196+
197+
func (d *Gitee) applyProxy(raw string) string {
198+
if raw == "" || d.DownloadProxy == "" {
199+
return raw
200+
}
201+
proxy := d.DownloadProxy
202+
if !strings.HasSuffix(proxy, "/") {
203+
proxy += "/"
204+
}
205+
return proxy + strings.TrimLeft(raw, "/")
206+
}
207+
208+
func encodePath(p string) string {
209+
if p == "" {
210+
return ""
211+
}
212+
parts := strings.Split(p, "/")
213+
for i, part := range parts {
214+
parts[i] = url.PathEscape(part)
215+
}
216+
return strings.Join(parts, "/")
217+
}
218+
219+
func joinPath(base, name string) string {
220+
if base == "" {
221+
return name
222+
}
223+
return strings.TrimPrefix(stdpath.Join(base, name), "./")
224+
}

drivers/gitee/meta.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package gitee
2+
3+
import (
4+
"github.com/alist-org/alist/v3/internal/driver"
5+
"github.com/alist-org/alist/v3/internal/op"
6+
)
7+
8+
type Addition struct {
9+
driver.RootPath
10+
Endpoint string `json:"endpoint" type:"string" help:"Gitee API endpoint, default https://gitee.com/api/v5"`
11+
Token string `json:"token" type:"string"`
12+
Owner string `json:"owner" type:"string" required:"true"`
13+
Repo string `json:"repo" type:"string" required:"true"`
14+
Ref string `json:"ref" type:"string" help:"Branch, tag or commit SHA, defaults to repository default branch"`
15+
DownloadProxy string `json:"download_proxy" type:"string" help:"Prefix added before download URLs, e.g. https://mirror.example.com/"`
16+
Cookie string `json:"cookie" type:"string" help:"Cookie returned from user info request"`
17+
}
18+
19+
var config = driver.Config{
20+
Name: "Gitee",
21+
LocalSort: true,
22+
DefaultRoot: "/",
23+
}
24+
25+
func init() {
26+
op.RegisterDriver(func() driver.Driver {
27+
return &Gitee{}
28+
})
29+
}

drivers/gitee/types.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package gitee
2+
3+
import (
4+
"time"
5+
6+
"github.com/alist-org/alist/v3/internal/model"
7+
)
8+
9+
type Links struct {
10+
Self string `json:"self"`
11+
Html string `json:"html"`
12+
}
13+
14+
type Content struct {
15+
Type string `json:"type"`
16+
Size *int64 `json:"size"`
17+
Name string `json:"name"`
18+
Path string `json:"path"`
19+
Sha string `json:"sha"`
20+
URL string `json:"url"`
21+
HtmlURL string `json:"html_url"`
22+
DownloadURL string `json:"download_url"`
23+
Links Links `json:"_links"`
24+
}
25+
26+
func (c Content) toModelObj() model.Obj {
27+
size := int64(0)
28+
if c.Size != nil {
29+
size = *c.Size
30+
}
31+
return &Object{
32+
Object: model.Object{
33+
ID: c.Path,
34+
Name: c.Name,
35+
Size: size,
36+
Modified: time.Unix(0, 0),
37+
IsFolder: c.Type == "dir",
38+
},
39+
DownloadURL: c.DownloadURL,
40+
HtmlURL: c.HtmlURL,
41+
}
42+
}
43+
44+
type Object struct {
45+
model.Object
46+
DownloadURL string
47+
HtmlURL string
48+
}
49+
50+
func (o *Object) URL() string {
51+
return o.DownloadURL
52+
}
53+
54+
type Repo struct {
55+
DefaultBranch string `json:"default_branch"`
56+
}
57+
58+
type ErrResp struct {
59+
Message string `json:"message"`
60+
}

drivers/gitee/util.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package gitee
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
7+
"github.com/alist-org/alist/v3/pkg/utils"
8+
"github.com/go-resty/resty/v2"
9+
)
10+
11+
func (d *Gitee) getRepo() (*Repo, error) {
12+
req := d.client.R()
13+
if d.Token != "" {
14+
req.SetQueryParam("access_token", d.Token)
15+
}
16+
if d.Cookie != "" {
17+
req.SetHeader("Cookie", d.Cookie)
18+
}
19+
escapedOwner := url.PathEscape(d.Owner)
20+
escapedRepo := url.PathEscape(d.Repo)
21+
res, err := req.Get(fmt.Sprintf("/repos/%s/%s", escapedOwner, escapedRepo))
22+
if err != nil {
23+
return nil, err
24+
}
25+
if res.IsError() {
26+
return nil, toErr(res)
27+
}
28+
var repo Repo
29+
if err := utils.Json.Unmarshal(res.Body(), &repo); err != nil {
30+
return nil, err
31+
}
32+
if repo.DefaultBranch == "" {
33+
return nil, fmt.Errorf("failed to fetch default branch")
34+
}
35+
return &repo, nil
36+
}
37+
38+
func toErr(res *resty.Response) error {
39+
var errMsg ErrResp
40+
if err := utils.Json.Unmarshal(res.Body(), &errMsg); err == nil && errMsg.Message != "" {
41+
return fmt.Errorf("%s: %s", res.Status(), errMsg.Message)
42+
}
43+
return fmt.Errorf(res.Status())
44+
}

0 commit comments

Comments
 (0)