mirror of
https://github.com/AlistGo/alist.git
synced 2025-12-18 18:30:10 +08:00
feat(driver): Added Gitee driver implementation
- Implemented a Gitee driver with support for core file operations: list, upload, move, copy, rename, and delete - Added functionality for managing trees and commits, including template-based commit messages - Integrated GPG signing for commits when private key is configured - Leveraged Gitee's API for seamless interaction with repositories
This commit is contained in:
parent
b4d9beb49c
commit
b4f6361771
@ -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"
|
||||
|
||||
1075
drivers/gitee/driver.go
Normal file
1075
drivers/gitee/driver.go
Normal file
File diff suppressed because it is too large
Load Diff
38
drivers/gitee/meta.go
Normal file
38
drivers/gitee/meta.go
Normal file
@ -0,0 +1,38 @@
|
||||
package gitee
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootPath
|
||||
Token string `json:"token" type:"string" required:"true"`
|
||||
Owner string `json:"owner" type:"string" required:"true"`
|
||||
Repo string `json:"repo" type:"string" required:"true"`
|
||||
Ref string `json:"ref" type:"string" help:"A branch, a tag or a commit SHA, main branch by default."`
|
||||
GPGPrivateKey string `json:"gpg_private_key" type:"text"`
|
||||
GPGKeyPassphrase string `json:"gpg_key_passphrase" type:"string"`
|
||||
CommitterName string `json:"committer_name" type:"string"`
|
||||
CommitterEmail string `json:"committer_email" type:"string"`
|
||||
AuthorName string `json:"author_name" type:"string"`
|
||||
AuthorEmail string `json:"author_email" type:"string"`
|
||||
MkdirCommitMsg string `json:"mkdir_commit_message" type:"text" default:"{{.UserName}} mkdir {{.ObjPath}}"`
|
||||
DeleteCommitMsg string `json:"delete_commit_message" type:"text" default:"{{.UserName}} remove {{.ObjPath}}"`
|
||||
PutCommitMsg string `json:"put_commit_message" type:"text" default:"{{.UserName}} upload {{.ObjPath}}"`
|
||||
RenameCommitMsg string `json:"rename_commit_message" type:"text" default:"{{.UserName}} rename {{.ObjPath}} to {{.TargetName}}"`
|
||||
CopyCommitMsg string `json:"copy_commit_message" type:"text" default:"{{.UserName}} copy {{.ObjPath}} to {{.TargetPath}}"`
|
||||
MoveCommitMsg string `json:"move_commit_message" type:"text" default:"{{.UserName}} move {{.ObjPath}} to {{.TargetPath}}"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Gitee API",
|
||||
LocalSort: true,
|
||||
DefaultRoot: "/",
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Gitee{}
|
||||
})
|
||||
}
|
||||
120
drivers/gitee/types.go
Normal file
120
drivers/gitee/types.go
Normal file
@ -0,0 +1,120 @@
|
||||
package gitee
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Links struct {
|
||||
Git string `json:"git"`
|
||||
Html string `json:"html"`
|
||||
Self string `json:"self"`
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
Type string `json:"type"`
|
||||
Encoding string `json:"encoding" required:"false"`
|
||||
Size int64 `json:"size"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content" required:"false"`
|
||||
Sha string `json:"sha"`
|
||||
URL string `json:"url"`
|
||||
GitURL string `json:"git_url"`
|
||||
HtmlURL string `json:"html_url"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
Entries []Object `json:"entries" required:"false"`
|
||||
Links Links `json:"_links"`
|
||||
SubmoduleGitURL string `json:"submodule_git_url" required:"false"`
|
||||
Target string `json:"target" required:"false"`
|
||||
}
|
||||
|
||||
func (o *Object) toModelObj() *model.Object {
|
||||
return &model.Object{
|
||||
Name: o.Name,
|
||||
Size: o.Size,
|
||||
Modified: time.Unix(0, 0),
|
||||
IsFolder: o.Type == "dir",
|
||||
}
|
||||
}
|
||||
|
||||
type PutBlobResp struct {
|
||||
URL string `json:"url"`
|
||||
Sha string `json:"sha"`
|
||||
}
|
||||
|
||||
type ErrResp struct {
|
||||
Message string `json:"message"`
|
||||
DocumentationURL string `json:"documentation_url"`
|
||||
Status string `json:"status"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
type TreeObjReq struct {
|
||||
Path string `json:"path"`
|
||||
Mode string `json:"mode"`
|
||||
Type string `json:"type"`
|
||||
Sha interface{} `json:"sha"`
|
||||
}
|
||||
|
||||
type TreeObjResp struct {
|
||||
TreeObjReq
|
||||
Size int64 `json:"size" required:"false"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func (o *TreeObjResp) toModelObj() *model.Object {
|
||||
return &model.Object{
|
||||
Name: o.Path,
|
||||
Size: o.Size,
|
||||
Modified: time.Unix(0, 0),
|
||||
IsFolder: o.Type == "tree",
|
||||
}
|
||||
}
|
||||
|
||||
type TreeResp struct {
|
||||
Sha string `json:"sha"`
|
||||
URL string `json:"url"`
|
||||
Trees []TreeObjResp `json:"tree"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
type TreeReq struct {
|
||||
BaseTree interface{} `json:"base_tree,omitempty"`
|
||||
Trees []interface{} `json:"tree"`
|
||||
}
|
||||
|
||||
type CommitResp struct {
|
||||
Sha string `json:"sha"`
|
||||
}
|
||||
|
||||
type CommitDetailResp struct {
|
||||
Sha string `json:"sha"`
|
||||
Tree struct {
|
||||
Sha string `json:"sha"`
|
||||
} `json:"tree"`
|
||||
Commit struct {
|
||||
Tree struct {
|
||||
Sha string `json:"sha"`
|
||||
} `json:"tree"`
|
||||
} `json:"commit"`
|
||||
}
|
||||
|
||||
type BranchResp struct {
|
||||
Name string `json:"name"`
|
||||
Commit CommitResp `json:"commit"`
|
||||
}
|
||||
|
||||
type UpdateRefReq struct {
|
||||
Sha string `json:"sha"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
type RepoResp struct {
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
}
|
||||
|
||||
type UserResp struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
171
drivers/gitee/util.go
Normal file
171
drivers/gitee/util.go
Normal file
@ -0,0 +1,171 @@
|
||||
package gitee
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type MessageTemplateVars struct {
|
||||
UserName string
|
||||
ObjName string
|
||||
ObjPath string
|
||||
ParentName string
|
||||
ParentPath string
|
||||
TargetName string
|
||||
TargetPath string
|
||||
}
|
||||
|
||||
func getMessage(tmpl *template.Template, vars *MessageTemplateVars, defaultOpStr string) (string, error) {
|
||||
sb := strings.Builder{}
|
||||
if err := tmpl.Execute(&sb, vars); err != nil {
|
||||
return fmt.Sprintf("%s %s %s", vars.UserName, defaultOpStr, vars.ObjPath), err
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func calculateBase64Length(inputLength int64) int64 {
|
||||
return 4 * ((inputLength + 2) / 3)
|
||||
}
|
||||
|
||||
func toErr(res *resty.Response) error {
|
||||
var errMsg ErrResp
|
||||
if err := utils.Json.Unmarshal(res.Body(), &errMsg); err != nil || errMsg.Message == "" {
|
||||
if err != nil {
|
||||
return errors.New(res.Status())
|
||||
}
|
||||
return fmt.Errorf("%s: %s", res.Status(), string(res.Body()))
|
||||
}
|
||||
if errMsg.Code != 0 {
|
||||
return fmt.Errorf("%s: %s (%d)", res.Status(), errMsg.Message, errMsg.Code)
|
||||
}
|
||||
return fmt.Errorf("%s: %s", res.Status(), errMsg.Message)
|
||||
}
|
||||
|
||||
// Example input:
|
||||
// a = /aaa/bbb/ccc
|
||||
// b = /aaa/b11/ddd/ccc
|
||||
//
|
||||
// Output:
|
||||
// ancestor = /aaa
|
||||
// aChildName = bbb
|
||||
// bChildName = b11
|
||||
// aRest = bbb/ccc
|
||||
// bRest = b11/ddd/ccc
|
||||
func getPathCommonAncestor(a, b string) (ancestor, aChildName, bChildName, aRest, bRest string) {
|
||||
a = utils.FixAndCleanPath(a)
|
||||
b = utils.FixAndCleanPath(b)
|
||||
idx := 1
|
||||
for idx < len(a) && idx < len(b) {
|
||||
if a[idx] != b[idx] {
|
||||
break
|
||||
}
|
||||
idx++
|
||||
}
|
||||
aNextIdx := idx
|
||||
for aNextIdx < len(a) {
|
||||
if a[aNextIdx] == '/' {
|
||||
break
|
||||
}
|
||||
aNextIdx++
|
||||
}
|
||||
bNextIdx := idx
|
||||
for bNextIdx < len(b) {
|
||||
if b[bNextIdx] == '/' {
|
||||
break
|
||||
}
|
||||
bNextIdx++
|
||||
}
|
||||
for idx > 0 {
|
||||
if a[idx] == '/' {
|
||||
break
|
||||
}
|
||||
idx--
|
||||
}
|
||||
ancestor = utils.FixAndCleanPath(a[:idx])
|
||||
aChildName = a[idx+1 : aNextIdx]
|
||||
bChildName = b[idx+1 : bNextIdx]
|
||||
aRest = a[idx+1:]
|
||||
bRest = b[idx+1:]
|
||||
return ancestor, aChildName, bChildName, aRest, bRest
|
||||
}
|
||||
|
||||
func getUsername(ctx context.Context) string {
|
||||
user, ok := ctx.Value("user").(*model.User)
|
||||
if !ok {
|
||||
return "<system>"
|
||||
}
|
||||
return user.Username
|
||||
}
|
||||
|
||||
func loadPrivateKey(key, passphrase string) (*openpgp.Entity, error) {
|
||||
entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(entityList) < 1 {
|
||||
return nil, fmt.Errorf("no keys found in key ring")
|
||||
}
|
||||
entity := entityList[0]
|
||||
|
||||
pass := []byte(passphrase)
|
||||
if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
|
||||
if err = entity.PrivateKey.Decrypt(pass); err != nil {
|
||||
return nil, fmt.Errorf("password incorrect: %+v", err)
|
||||
}
|
||||
}
|
||||
for _, subKey := range entity.Subkeys {
|
||||
if subKey.PrivateKey != nil && subKey.PrivateKey.Encrypted {
|
||||
if err = subKey.PrivateKey.Decrypt(pass); err != nil {
|
||||
return nil, fmt.Errorf("password incorrect: %+v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
func signCommit(m *map[string]interface{}, entity *openpgp.Entity) (string, error) {
|
||||
var commit strings.Builder
|
||||
commit.WriteString(fmt.Sprintf("tree %s\n", (*m)["tree"].(string)))
|
||||
parents := (*m)["parents"].([]string)
|
||||
for _, p := range parents {
|
||||
commit.WriteString(fmt.Sprintf("parent %s\n", p))
|
||||
}
|
||||
now := time.Now()
|
||||
_, offset := now.Zone()
|
||||
hour := offset / 3600
|
||||
author := (*m)["author"].(map[string]string)
|
||||
commit.WriteString(fmt.Sprintf("author %s <%s> %d %+03d00\n", author["name"], author["email"], now.Unix(), hour))
|
||||
author["date"] = now.Format(time.RFC3339)
|
||||
committer := (*m)["committer"].(map[string]string)
|
||||
commit.WriteString(fmt.Sprintf("committer %s <%s> %d %+03d00\n", committer["name"], committer["email"], now.Unix(), hour))
|
||||
committer["date"] = now.Format(time.RFC3339)
|
||||
commit.WriteString(fmt.Sprintf("\n%s", (*m)["message"].(string)))
|
||||
data := commit.String()
|
||||
|
||||
var sigBuffer bytes.Buffer
|
||||
err := openpgp.DetachSign(&sigBuffer, entity, strings.NewReader(data), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("signing failed: %v", err)
|
||||
}
|
||||
var armoredSig bytes.Buffer
|
||||
armorWriter, err := armor.Encode(&armoredSig, "PGP SIGNATURE", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = utils.CopyWithBuffer(armorWriter, &sigBuffer); err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = armorWriter.Close()
|
||||
return armoredSig.String(), nil
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user