Skip to content

Commit f1a5048

Browse files
xrgzsILoveScratch2
andauthored
feat(drivers): add cnb_releases (#1033)
* feat(drivers): add cnb_releases * feat(cnb_release): implement reference * refactor(cnb_releases): get release info by ID instead of tag name * feat(cnb_releases): add option to use tag name instead of release name * fix(cnb_releases): set default root and improve release info retrieval * feat(cnb_releases): implement Put * perf(cnb_release): use io.Pipe to stream file upload * perf(cnb_releases): add context timeout for file upload request * feat(cnb_releases): implement Remove * feat(cnb_releases): implement MakeDir * feat(cnb_releases): implement Rename * feat(cnb_releases): require repo and token in Addition * chore(cnb_releases): remove unused code * Revert 'perf(cnb_release): use io.Pipe to stream file upload' * perf(cnb_releases): optimize upload with MultiReader * feat(cnb_releases): add DefaultBranch --------- Co-authored-by: ILoveScratch <[email protected]>
1 parent 1fe26bf commit f1a5048

File tree

5 files changed

+415
-0
lines changed

5 files changed

+415
-0
lines changed

drivers/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
_ "github.com/OpenListTeam/OpenList/v4/drivers/chaoxing"
2323
_ "github.com/OpenListTeam/OpenList/v4/drivers/cloudreve"
2424
_ "github.com/OpenListTeam/OpenList/v4/drivers/cloudreve_v4"
25+
_ "github.com/OpenListTeam/OpenList/v4/drivers/cnb_releases"
2526
_ "github.com/OpenListTeam/OpenList/v4/drivers/crypt"
2627
_ "github.com/OpenListTeam/OpenList/v4/drivers/degoo"
2728
_ "github.com/OpenListTeam/OpenList/v4/drivers/doubao"

drivers/cnb_releases/driver.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package cnb_releases
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"mime/multipart"
9+
"net/http"
10+
"time"
11+
12+
"github.com/OpenListTeam/OpenList/v4/drivers/base"
13+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
14+
"github.com/OpenListTeam/OpenList/v4/internal/errs"
15+
"github.com/OpenListTeam/OpenList/v4/internal/model"
16+
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
17+
"github.com/go-resty/resty/v2"
18+
)
19+
20+
type CnbReleases struct {
21+
model.Storage
22+
Addition
23+
ref *CnbReleases
24+
}
25+
26+
func (d *CnbReleases) Config() driver.Config {
27+
return config
28+
}
29+
30+
func (d *CnbReleases) GetAddition() driver.Additional {
31+
return &d.Addition
32+
}
33+
34+
func (d *CnbReleases) Init(ctx context.Context) error {
35+
return nil
36+
}
37+
38+
func (d *CnbReleases) InitReference(storage driver.Driver) error {
39+
refStorage, ok := storage.(*CnbReleases)
40+
if ok {
41+
d.ref = refStorage
42+
return nil
43+
}
44+
return fmt.Errorf("ref: storage is not CnbReleases")
45+
}
46+
47+
func (d *CnbReleases) Drop(ctx context.Context) error {
48+
d.ref = nil
49+
return nil
50+
}
51+
52+
func (d *CnbReleases) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
53+
if dir.GetPath() == "/" {
54+
// get all releases for root dir
55+
var resp ReleaseList
56+
57+
err := d.Request(http.MethodGet, "/{repo}/-/releases", func(req *resty.Request) {
58+
req.SetPathParam("repo", d.Repo)
59+
}, &resp)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
return utils.SliceConvert(resp, func(src Release) (model.Obj, error) {
65+
name := src.Name
66+
if d.UseTagName {
67+
name = src.TagName
68+
}
69+
return &model.Object{
70+
ID: src.ID,
71+
Name: name,
72+
Size: d.sumAssetsSize(src.Assets),
73+
Ctime: src.CreatedAt,
74+
Modified: src.UpdatedAt,
75+
IsFolder: true,
76+
}, nil
77+
})
78+
} else {
79+
// get release info by release id
80+
releaseID := dir.GetID()
81+
if releaseID == "" {
82+
return nil, errs.ObjectNotFound
83+
}
84+
var resp Release
85+
err := d.Request(http.MethodGet, "/{repo}/-/releases/{release_id}", func(req *resty.Request) {
86+
req.SetPathParam("repo", d.Repo)
87+
req.SetPathParam("release_id", releaseID)
88+
}, &resp)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
return utils.SliceConvert(resp.Assets, func(src ReleaseAsset) (model.Obj, error) {
94+
return &Object{
95+
Object: model.Object{
96+
ID: src.ID,
97+
Path: src.Path,
98+
Name: src.Name,
99+
Size: src.Size,
100+
Ctime: src.CreatedAt,
101+
Modified: src.UpdatedAt,
102+
IsFolder: false,
103+
},
104+
ParentID: dir.GetID(),
105+
}, nil
106+
})
107+
}
108+
}
109+
110+
func (d *CnbReleases) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
111+
return &model.Link{
112+
URL: "https://cnb.cool" + file.GetPath(),
113+
}, nil
114+
}
115+
116+
func (d *CnbReleases) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
117+
if parentDir.GetPath() == "/" {
118+
// create a new release
119+
branch := d.DefaultBranch
120+
if branch == "" {
121+
branch = "main" // fallback to "main" if not set
122+
}
123+
return d.Request(http.MethodPost, "/{repo}/-/releases", func(req *resty.Request) {
124+
req.SetPathParam("repo", d.Repo)
125+
req.SetBody(base.Json{
126+
"name": dirName,
127+
"tag_name": dirName,
128+
"target_commitish": branch,
129+
})
130+
}, nil)
131+
}
132+
return errs.NotImplement
133+
}
134+
135+
func (d *CnbReleases) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
136+
return nil, errs.NotImplement
137+
}
138+
139+
func (d *CnbReleases) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
140+
if srcObj.IsDir() && !d.UseTagName {
141+
return d.Request(http.MethodPatch, "/{repo}/-/releases/{release_id}", func(req *resty.Request) {
142+
req.SetPathParam("repo", d.Repo)
143+
req.SetPathParam("release_id", srcObj.GetID())
144+
req.SetFormData(map[string]string{
145+
"name": newName,
146+
})
147+
}, nil)
148+
}
149+
return errs.NotImplement
150+
}
151+
152+
func (d *CnbReleases) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
153+
return nil, errs.NotImplement
154+
}
155+
156+
func (d *CnbReleases) Remove(ctx context.Context, obj model.Obj) error {
157+
if obj.IsDir() {
158+
return d.Request(http.MethodDelete, "/{repo}/-/releases/{release_id}", func(req *resty.Request) {
159+
req.SetPathParam("repo", d.Repo)
160+
req.SetPathParam("release_id", obj.GetID())
161+
}, nil)
162+
}
163+
if o, ok := obj.(*Object); ok {
164+
return d.Request(http.MethodDelete, "/{repo}/-/releases/{release_id}/assets/{asset_id}", func(req *resty.Request) {
165+
req.SetPathParam("repo", d.Repo)
166+
req.SetPathParam("release_id", o.ParentID)
167+
req.SetPathParam("asset_id", obj.GetID())
168+
}, nil)
169+
} else {
170+
return fmt.Errorf("unable to get release ID")
171+
}
172+
}
173+
174+
func (d *CnbReleases) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
175+
// 1. get upload info
176+
var resp ReleaseAssetUploadURL
177+
err := d.Request(http.MethodPost, "/{repo}/-/releases/{release_id}/asset-upload-url", func(req *resty.Request) {
178+
req.SetPathParam("repo", d.Repo)
179+
req.SetPathParam("release_id", dstDir.GetID())
180+
req.SetBody(base.Json{
181+
"asset_name": file.GetName(),
182+
"overwrite": true,
183+
"size": file.GetSize(),
184+
})
185+
}, &resp)
186+
if err != nil {
187+
return err
188+
}
189+
190+
// 2. upload file
191+
// use multipart to create form file
192+
var b bytes.Buffer
193+
w := multipart.NewWriter(&b)
194+
_, err = w.CreateFormFile("file", file.GetName())
195+
if err != nil {
196+
return err
197+
}
198+
headSize := b.Len()
199+
err = w.Close()
200+
if err != nil {
201+
return err
202+
}
203+
204+
head := bytes.NewReader(b.Bytes()[:headSize])
205+
tail := bytes.NewReader(b.Bytes()[headSize:])
206+
rateLimitedRd := driver.NewLimitedUploadStream(ctx, io.MultiReader(head, file, tail))
207+
208+
// use net/http to upload file
209+
ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Duration(resp.ExpiresInSec+1)*time.Second)
210+
defer cancel()
211+
req, err := http.NewRequestWithContext(ctxWithTimeout, http.MethodPost, resp.UploadURL, rateLimitedRd)
212+
if err != nil {
213+
return err
214+
}
215+
req.Header.Set("Content-Type", w.FormDataContentType())
216+
req.Header.Set("User-Agent", base.UserAgent)
217+
httpResp, err := base.HttpClient.Do(req)
218+
if err != nil {
219+
return err
220+
}
221+
defer httpResp.Body.Close()
222+
if httpResp.StatusCode != http.StatusNoContent {
223+
return fmt.Errorf("upload file failed: %s", httpResp.Status)
224+
}
225+
226+
// 3. verify upload
227+
return d.Request(http.MethodPost, resp.VerifyURL, nil, nil)
228+
}
229+
230+
var _ driver.Driver = (*CnbReleases)(nil)

drivers/cnb_releases/meta.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package cnb_releases
2+
3+
import (
4+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
5+
"github.com/OpenListTeam/OpenList/v4/internal/op"
6+
)
7+
8+
type Addition struct {
9+
driver.RootPath
10+
Repo string `json:"repo" type:"string" required:"true"`
11+
Token string `json:"token" type:"string" required:"true"`
12+
UseTagName bool `json:"use_tag_name" type:"bool" default:"false" help:"Use tag name instead of release name"`
13+
DefaultBranch string `json:"default_branch" type:"string" default:"main" help:"Default branch for new releases"`
14+
}
15+
16+
var config = driver.Config{
17+
Name: "CNB Releases",
18+
LocalSort: true,
19+
DefaultRoot: "/",
20+
}
21+
22+
func init() {
23+
op.RegisterDriver(func() driver.Driver {
24+
return &CnbReleases{}
25+
})
26+
}

drivers/cnb_releases/types.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package cnb_releases
2+
3+
import (
4+
"time"
5+
6+
"github.com/OpenListTeam/OpenList/v4/internal/model"
7+
)
8+
9+
type Object struct {
10+
model.Object
11+
ParentID string
12+
}
13+
14+
type TagList []Tag
15+
16+
type Tag struct {
17+
Commit struct {
18+
Author UserInfo `json:"author"`
19+
Commit CommitObject `json:"commit"`
20+
Committer UserInfo `json:"committer"`
21+
Parents []CommitParent `json:"parents"`
22+
Sha string `json:"sha"`
23+
} `json:"commit"`
24+
Name string `json:"name"`
25+
Target string `json:"target"`
26+
TargetType string `json:"target_type"`
27+
Verification TagObjectVerification `json:"verification"`
28+
}
29+
30+
type UserInfo struct {
31+
Freeze bool `json:"freeze"`
32+
Nickname string `json:"nickname"`
33+
Username string `json:"username"`
34+
}
35+
36+
type CommitObject struct {
37+
Author Signature `json:"author"`
38+
CommentCount int `json:"comment_count"`
39+
Committer Signature `json:"committer"`
40+
Message string `json:"message"`
41+
Tree CommitObjectTree `json:"tree"`
42+
Verification CommitObjectVerification `json:"verification"`
43+
}
44+
45+
type Signature struct {
46+
Date time.Time `json:"date"`
47+
Email string `json:"email"`
48+
Name string `json:"name"`
49+
}
50+
51+
type CommitObjectTree struct {
52+
Sha string `json:"sha"`
53+
}
54+
55+
type CommitObjectVerification struct {
56+
Payload string `json:"payload"`
57+
Reason string `json:"reason"`
58+
Signature string `json:"signature"`
59+
Verified bool `json:"verified"`
60+
VerifiedAt string `json:"verified_at"`
61+
}
62+
63+
type CommitParent = CommitObjectTree
64+
65+
type TagObjectVerification = CommitObjectVerification
66+
67+
type ReleaseList []Release
68+
69+
type Release struct {
70+
Assets []ReleaseAsset `json:"assets"`
71+
Author UserInfo `json:"author"`
72+
Body string `json:"body"`
73+
CreatedAt time.Time `json:"created_at"`
74+
Draft bool `json:"draft"`
75+
ID string `json:"id"`
76+
IsLatest bool `json:"is_latest"`
77+
Name string `json:"name"`
78+
Prerelease bool `json:"prerelease"`
79+
PublishedAt time.Time `json:"published_at"`
80+
TagCommitish string `json:"tag_commitish"`
81+
TagName string `json:"tag_name"`
82+
UpdatedAt time.Time `json:"updated_at"`
83+
}
84+
85+
type ReleaseAsset struct {
86+
ContentType string `json:"content_type"`
87+
CreatedAt time.Time `json:"created_at"`
88+
ID string `json:"id"`
89+
Name string `json:"name"`
90+
Path string `json:"path"`
91+
Size int64 `json:"size"`
92+
UpdatedAt time.Time `json:"updated_at"`
93+
Uploader UserInfo `json:"uploader"`
94+
}
95+
96+
type ReleaseAssetUploadURL struct {
97+
UploadURL string `json:"upload_url"`
98+
ExpiresInSec int `json:"expires_in_sec"`
99+
VerifyURL string `json:"verify_url"`
100+
}

0 commit comments

Comments
 (0)