feat(github): add github api driver (#7717)

* feat(github): add github api driver

* fix: filter submodule operation

* feat: rename, copy and move, but with bugs

* fix: move and copy returns 422

* fix: change TargetPath in rename msg from parent path to new self path

* fix: add non-commit mutex

* pref(github): use net/http to put blob

* chore: add a help message to `ref` addition
pull/7326/merge
KirCute_ECT 2025-01-10 20:59:58 +08:00 committed by GitHub
parent 51bcf83511
commit e04114d102
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1182 additions and 0 deletions

View File

@ -24,6 +24,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/dropbox" _ "github.com/alist-org/alist/v3/drivers/dropbox"
_ "github.com/alist-org/alist/v3/drivers/febbox" _ "github.com/alist-org/alist/v3/drivers/febbox"
_ "github.com/alist-org/alist/v3/drivers/ftp" _ "github.com/alist-org/alist/v3/drivers/ftp"
_ "github.com/alist-org/alist/v3/drivers/github"
_ "github.com/alist-org/alist/v3/drivers/google_drive" _ "github.com/alist-org/alist/v3/drivers/google_drive"
_ "github.com/alist-org/alist/v3/drivers/google_photo" _ "github.com/alist-org/alist/v3/drivers/google_photo"
_ "github.com/alist-org/alist/v3/drivers/halalcloud" _ "github.com/alist-org/alist/v3/drivers/halalcloud"

928
drivers/github/driver.go Normal file
View File

@ -0,0 +1,928 @@
package github
import (
"context"
"encoding/base64"
"errors"
"fmt"
"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"
log "github.com/sirupsen/logrus"
"io"
"net/http"
stdpath "path"
"strings"
"sync"
"text/template"
)
type Github struct {
model.Storage
Addition
client *resty.Client
mkdirMsgTmpl *template.Template
deleteMsgTmpl *template.Template
putMsgTmpl *template.Template
renameMsgTmpl *template.Template
copyMsgTmpl *template.Template
moveMsgTmpl *template.Template
isOnBranch bool
commitMutex sync.Mutex
}
func (d *Github) Config() driver.Config {
return config
}
func (d *Github) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Github) Init(ctx context.Context) error {
d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)
if d.CommitterName != "" && d.CommitterEmail == "" {
return errors.New("committer email is required")
}
if d.CommitterName == "" && d.CommitterEmail != "" {
return errors.New("committer name is required")
}
if d.AuthorName != "" && d.AuthorEmail == "" {
return errors.New("author email is required")
}
if d.AuthorName == "" && d.AuthorEmail != "" {
return errors.New("author name is required")
}
var err error
d.mkdirMsgTmpl, err = template.New("mkdirCommitMsgTemplate").Parse(d.MkdirCommitMsg)
if err != nil {
return err
}
d.deleteMsgTmpl, err = template.New("deleteCommitMsgTemplate").Parse(d.DeleteCommitMsg)
if err != nil {
return err
}
d.putMsgTmpl, err = template.New("putCommitMsgTemplate").Parse(d.PutCommitMsg)
if err != nil {
return err
}
d.renameMsgTmpl, err = template.New("renameCommitMsgTemplate").Parse(d.RenameCommitMsg)
if err != nil {
return err
}
d.copyMsgTmpl, err = template.New("copyCommitMsgTemplate").Parse(d.CopyCommitMsg)
if err != nil {
return err
}
d.moveMsgTmpl, err = template.New("moveCommitMsgTemplate").Parse(d.MoveCommitMsg)
if err != nil {
return err
}
d.client = base.NewRestyClient().
SetHeader("Accept", "application/vnd.github.object+json").
SetHeader("Authorization", "Bearer "+d.Token).
SetHeader("X-GitHub-Api-Version", "2022-11-28").
SetLogger(log.StandardLogger()).
SetDebug(false)
if d.Ref == "" {
repo, err := d.getRepo()
if err != nil {
return err
}
d.Ref = repo.DefaultBranch
d.isOnBranch = true
} else {
_, err = d.getBranchHead()
d.isOnBranch = err == nil
}
return nil
}
func (d *Github) Drop(ctx context.Context) error {
return nil
}
func (d *Github) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
obj, err := d.get(dir.GetPath())
if err != nil {
return nil, err
}
if obj.Entries == nil {
return nil, errs.NotFolder
}
if len(obj.Entries) >= 1000 {
tree, err := d.getTree(obj.Sha)
if err != nil {
return nil, err
}
if tree.Truncated {
return nil, fmt.Errorf("tree %s is truncated", dir.GetPath())
}
ret := make([]model.Obj, 0, len(tree.Trees))
for _, t := range tree.Trees {
if t.Path != ".gitkeep" {
ret = append(ret, t.toModelObj())
}
}
return ret, nil
} else {
ret := make([]model.Obj, 0, len(obj.Entries))
for _, entry := range obj.Entries {
if entry.Name != ".gitkeep" {
ret = append(ret, entry.toModelObj())
}
}
return ret, nil
}
}
func (d *Github) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
obj, err := d.get(file.GetPath())
if err != nil {
return nil, err
}
if obj.Type == "submodule" {
return nil, errors.New("cannot download a submodule")
}
return &model.Link{
URL: obj.DownloadURL,
}, nil
}
func (d *Github) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if !d.isOnBranch {
return errors.New("cannot write to non-branch reference")
}
d.commitMutex.Lock()
defer d.commitMutex.Unlock()
parent, err := d.get(parentDir.GetPath())
if err != nil {
return err
}
if parent.Entries == nil {
return errs.NotFolder
}
// if parent folder contains .gitkeep only, mark it and delete .gitkeep later
gitKeepSha := ""
if len(parent.Entries) == 1 && parent.Entries[0].Name == ".gitkeep" {
gitKeepSha = parent.Entries[0].Sha
}
commitMessage, err := getMessage(d.mkdirMsgTmpl, &MessageTemplateVars{
UserName: getUsername(ctx),
ObjName: dirName,
ObjPath: stdpath.Join(parentDir.GetPath(), dirName),
ParentName: parentDir.GetName(),
ParentPath: parentDir.GetPath(),
}, "mkdir")
if err != nil {
return err
}
if err = d.createGitKeep(stdpath.Join(parentDir.GetPath(), dirName), commitMessage); err != nil {
return err
}
if gitKeepSha != "" {
err = d.delete(stdpath.Join(parentDir.GetPath(), ".gitkeep"), gitKeepSha, commitMessage)
}
return err
}
func (d *Github) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
if !d.isOnBranch {
return errors.New("cannot write to non-branch reference")
}
if strings.HasPrefix(dstDir.GetPath(), srcObj.GetPath()) {
return errors.New("cannot move parent dir to child")
}
d.commitMutex.Lock()
defer d.commitMutex.Unlock()
var rootSha string
if strings.HasPrefix(dstDir.GetPath(), stdpath.Dir(srcObj.GetPath())) { // /aa/1 -> /aa/bb/
dstOldSha, dstNewSha, ancestorOldSha, srcParentTree, err := d.copyWithoutRenewTree(srcObj, dstDir)
if err != nil {
return err
}
srcParentPath := stdpath.Dir(srcObj.GetPath())
dstRest := dstDir.GetPath()[len(srcParentPath):]
if dstRest[0] == '/' {
dstRest = dstRest[1:]
}
dstNextName, _, _ := strings.Cut(dstRest, "/")
dstNextPath := stdpath.Join(srcParentPath, dstNextName)
dstNextTreeSha, err := d.renewParentTrees(dstDir.GetPath(), dstOldSha, dstNewSha, dstNextPath)
if err != nil {
return err
}
var delSrc, dstNextTree *TreeObjReq = nil, nil
for _, t := range srcParentTree.Trees {
if t.Path == dstNextName {
dstNextTree = &t.TreeObjReq
dstNextTree.Sha = dstNextTreeSha
}
if t.Path == srcObj.GetName() {
delSrc = &t.TreeObjReq
delSrc.Sha = nil
}
if delSrc != nil && dstNextTree != nil {
break
}
}
if delSrc == nil || dstNextTree == nil {
return errs.ObjectNotFound
}
ancestorNewSha, err := d.newTree(ancestorOldSha, []interface{}{*delSrc, *dstNextTree})
if err != nil {
return err
}
rootSha, err = d.renewParentTrees(srcParentPath, ancestorOldSha, ancestorNewSha, "/")
if err != nil {
return err
}
} else if strings.HasPrefix(srcObj.GetPath(), dstDir.GetPath()) { // /aa/bb/1 -> /aa/
srcParentPath := stdpath.Dir(srcObj.GetPath())
srcParentTree, srcParentOldSha, err := d.getTreeDirectly(srcParentPath)
if err != nil {
return err
}
var src *TreeObjReq = nil
for _, t := range srcParentTree.Trees {
if t.Path == srcObj.GetName() {
if t.Type == "commit" {
return errors.New("cannot move a submodule")
}
src = &t.TreeObjReq
break
}
}
if src == nil {
return errs.ObjectNotFound
}
delSrc := *src
delSrc.Sha = nil
delSrcTree := make([]interface{}, 0, 2)
delSrcTree = append(delSrcTree, delSrc)
if len(srcParentTree.Trees) == 1 {
delSrcTree = append(delSrcTree, map[string]string{
"path": ".gitkeep",
"mode": "100644",
"type": "blob",
"content": "",
})
}
srcParentNewSha, err := d.newTree(srcParentOldSha, delSrcTree)
if err != nil {
return err
}
srcRest := srcObj.GetPath()[len(dstDir.GetPath()):]
if srcRest[0] == '/' {
srcRest = srcRest[1:]
}
srcNextName, _, ok := strings.Cut(srcRest, "/")
if !ok { // /aa/1 -> /aa/
return errors.New("cannot move in place")
}
srcNextPath := stdpath.Join(dstDir.GetPath(), srcNextName)
srcNextTreeSha, err := d.renewParentTrees(srcParentPath, srcParentOldSha, srcParentNewSha, srcNextPath)
if err != nil {
return err
}
ancestorTree, ancestorOldSha, err := d.getTreeDirectly(dstDir.GetPath())
if err != nil {
return err
}
var srcNextTree *TreeObjReq = nil
for _, t := range ancestorTree.Trees {
if t.Path == srcNextName {
srcNextTree = &t.TreeObjReq
srcNextTree.Sha = srcNextTreeSha
break
}
}
if srcNextTree == nil {
return errs.ObjectNotFound
}
ancestorNewSha, err := d.newTree(ancestorOldSha, []interface{}{*srcNextTree, *src})
if err != nil {
return err
}
rootSha, err = d.renewParentTrees(dstDir.GetPath(), ancestorOldSha, ancestorNewSha, "/")
if err != nil {
return err
}
} else { // /aa/1 -> /bb/
// do copy
dstOldSha, dstNewSha, srcParentOldSha, srcParentTree, err := d.copyWithoutRenewTree(srcObj, dstDir)
if err != nil {
return err
}
// delete src object and create new tree
var srcNewTree *TreeObjReq = nil
for _, t := range srcParentTree.Trees {
if t.Path == srcObj.GetName() {
srcNewTree = &t.TreeObjReq
srcNewTree.Sha = nil
break
}
}
if srcNewTree == nil {
return errs.ObjectNotFound
}
delSrcTree := make([]interface{}, 0, 2)
delSrcTree = append(delSrcTree, *srcNewTree)
if len(srcParentTree.Trees) == 1 {
delSrcTree = append(delSrcTree, map[string]string{
"path": ".gitkeep",
"mode": "100644",
"type": "blob",
"content": "",
})
}
srcParentNewSha, err := d.newTree(srcParentOldSha, delSrcTree)
if err != nil {
return err
}
// renew but the common ancestor of srcPath and dstPath
ancestor, srcChildName, dstChildName, _, _ := getPathCommonAncestor(srcObj.GetPath(), dstDir.GetPath())
dstNextTreeSha, err := d.renewParentTrees(dstDir.GetPath(), dstOldSha, dstNewSha, stdpath.Join(ancestor, dstChildName))
if err != nil {
return err
}
srcNextTreeSha, err := d.renewParentTrees(stdpath.Dir(srcObj.GetPath()), srcParentOldSha, srcParentNewSha, stdpath.Join(ancestor, srcChildName))
if err != nil {
return err
}
// renew the tree of the last common ancestor
ancestorTree, ancestorOldSha, err := d.getTreeDirectly(ancestor)
if err != nil {
return err
}
newTree := make([]interface{}, 2)
srcBind := false
dstBind := false
for _, t := range ancestorTree.Trees {
if t.Path == srcChildName {
t.Sha = srcNextTreeSha
newTree[0] = t.TreeObjReq
srcBind = true
}
if t.Path == dstChildName {
t.Sha = dstNextTreeSha
newTree[1] = t.TreeObjReq
dstBind = true
}
if srcBind && dstBind {
break
}
}
if !srcBind || !dstBind {
return errs.ObjectNotFound
}
ancestorNewSha, err := d.newTree(ancestorOldSha, newTree)
if err != nil {
return err
}
// renew until root
rootSha, err = d.renewParentTrees(ancestor, ancestorOldSha, ancestorNewSha, "/")
if err != nil {
return err
}
}
// commit
message, err := getMessage(d.moveMsgTmpl, &MessageTemplateVars{
UserName: getUsername(ctx),
ObjName: srcObj.GetName(),
ObjPath: srcObj.GetPath(),
ParentName: stdpath.Base(stdpath.Dir(srcObj.GetPath())),
ParentPath: stdpath.Dir(srcObj.GetPath()),
TargetName: stdpath.Base(dstDir.GetPath()),
TargetPath: dstDir.GetPath(),
}, "move")
if err != nil {
return err
}
return d.commit(message, rootSha)
}
func (d *Github) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
if !d.isOnBranch {
return errors.New("cannot write to non-branch reference")
}
d.commitMutex.Lock()
defer d.commitMutex.Unlock()
parentDir := stdpath.Dir(srcObj.GetPath())
tree, _, err := d.getTreeDirectly(parentDir)
if err != nil {
return err
}
newTree := make([]interface{}, 2)
operated := false
for _, t := range tree.Trees {
if t.Path == srcObj.GetName() {
if t.Type == "commit" {
return errors.New("cannot rename a submodule")
}
delCopy := t.TreeObjReq
delCopy.Sha = nil
newTree[0] = delCopy
t.Path = newName
newTree[1] = t.TreeObjReq
operated = true
break
}
}
if !operated {
return errs.ObjectNotFound
}
newSha, err := d.newTree(tree.Sha, newTree)
if err != nil {
return err
}
rootSha, err := d.renewParentTrees(parentDir, tree.Sha, newSha, "/")
if err != nil {
return err
}
message, err := getMessage(d.renameMsgTmpl, &MessageTemplateVars{
UserName: getUsername(ctx),
ObjName: srcObj.GetName(),
ObjPath: srcObj.GetPath(),
ParentName: stdpath.Base(parentDir),
ParentPath: parentDir,
TargetName: newName,
TargetPath: stdpath.Join(parentDir, newName),
}, "rename")
if err != nil {
return err
}
return d.commit(message, rootSha)
}
func (d *Github) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
if !d.isOnBranch {
return errors.New("cannot write to non-branch reference")
}
if strings.HasPrefix(dstDir.GetPath(), srcObj.GetPath()) {
return errors.New("cannot copy parent dir to child")
}
d.commitMutex.Lock()
defer d.commitMutex.Unlock()
dstSha, newSha, _, _, err := d.copyWithoutRenewTree(srcObj, dstDir)
if err != nil {
return err
}
rootSha, err := d.renewParentTrees(dstDir.GetPath(), dstSha, newSha, "/")
if err != nil {
return err
}
message, err := getMessage(d.copyMsgTmpl, &MessageTemplateVars{
UserName: getUsername(ctx),
ObjName: srcObj.GetName(),
ObjPath: srcObj.GetPath(),
ParentName: stdpath.Base(stdpath.Dir(srcObj.GetPath())),
ParentPath: stdpath.Dir(srcObj.GetPath()),
TargetName: stdpath.Base(dstDir.GetPath()),
TargetPath: dstDir.GetPath(),
}, "copy")
if err != nil {
return err
}
return d.commit(message, rootSha)
}
func (d *Github) Remove(ctx context.Context, obj model.Obj) error {
if !d.isOnBranch {
return errors.New("cannot write to non-branch reference")
}
d.commitMutex.Lock()
defer d.commitMutex.Unlock()
parentDir := stdpath.Dir(obj.GetPath())
tree, treeSha, err := d.getTreeDirectly(parentDir)
if err != nil {
return err
}
var del *TreeObjReq = nil
for _, t := range tree.Trees {
if t.Path == obj.GetName() {
if t.Type == "commit" {
return errors.New("cannot remove a submodule")
}
del = &t.TreeObjReq
del.Sha = nil
break
}
}
if del == nil {
return errs.ObjectNotFound
}
newTree := make([]interface{}, 0, 2)
newTree = append(newTree, *del)
if len(tree.Trees) == 1 { // completely emptying the repository will get a 404
newTree = append(newTree, map[string]string{
"path": ".gitkeep",
"mode": "100644",
"type": "blob",
"content": "",
})
}
newSha, err := d.newTree(treeSha, newTree)
if err != nil {
return err
}
rootSha, err := d.renewParentTrees(parentDir, treeSha, newSha, "/")
if err != nil {
return err
}
commitMessage, err := getMessage(d.deleteMsgTmpl, &MessageTemplateVars{
UserName: getUsername(ctx),
ObjName: obj.GetName(),
ObjPath: obj.GetPath(),
ParentName: stdpath.Base(parentDir),
ParentPath: parentDir,
}, "remove")
if err != nil {
return err
}
return d.commit(commitMessage, rootSha)
}
func (d *Github) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
if !d.isOnBranch {
return errors.New("cannot write to non-branch reference")
}
blob, err := d.putBlob(ctx, stream, up)
if err != nil {
return err
}
d.commitMutex.Lock()
defer d.commitMutex.Unlock()
parent, err := d.get(dstDir.GetPath())
if err != nil {
return err
}
if parent.Entries == nil {
return errs.NotFolder
}
newTree := make([]interface{}, 0, 2)
newTree = append(newTree, TreeObjReq{
Path: stream.GetName(),
Mode: "100644",
Type: "blob",
Sha: blob,
})
if len(parent.Entries) == 1 && parent.Entries[0].Name == ".gitkeep" {
newTree = append(newTree, TreeObjReq{
Path: ".gitkeep",
Mode: "100644",
Type: "blob",
Sha: nil,
})
}
newSha, err := d.newTree(parent.Sha, newTree)
if err != nil {
return err
}
rootSha, err := d.renewParentTrees(dstDir.GetPath(), parent.Sha, newSha, "/")
if err != nil {
return err
}
commitMessage, err := getMessage(d.putMsgTmpl, &MessageTemplateVars{
UserName: getUsername(ctx),
ObjName: stream.GetName(),
ObjPath: stdpath.Join(dstDir.GetPath(), stream.GetName()),
ParentName: dstDir.GetName(),
ParentPath: dstDir.GetPath(),
}, "upload")
if err != nil {
return err
}
return d.commit(commitMessage, rootSha)
}
var _ driver.Driver = (*Github)(nil)
func (d *Github) getContentApiUrl(path string) string {
path = utils.FixAndCleanPath(path)
return fmt.Sprintf("https://api.github.com/repos/%s/%s/contents%s", d.Owner, d.Repo, path)
}
func (d *Github) get(path string) (*Object, error) {
res, err := d.client.R().SetQueryParam("ref", d.Ref).Get(d.getContentApiUrl(path))
if err != nil {
return nil, err
}
if res.StatusCode() != 200 {
return nil, toErr(res)
}
var resp Object
err = utils.Json.Unmarshal(res.Body(), &resp)
return &resp, err
}
func (d *Github) createGitKeep(path, message string) error {
body := map[string]interface{}{
"message": message,
"content": "",
"branch": d.Ref,
}
d.addCommitterAndAuthor(&body)
res, err := d.client.R().SetBody(body).Put(d.getContentApiUrl(stdpath.Join(path, ".gitkeep")))
if err != nil {
return err
}
if res.StatusCode() != 200 && res.StatusCode() != 201 {
return toErr(res)
}
return nil
}
func (d *Github) putBlob(ctx context.Context, stream model.FileStreamer, up driver.UpdateProgress) (string, error) {
beforeContent := "{\"encoding\":\"base64\",\"content\":\""
afterContent := "\"}"
length := int64(len(beforeContent)) + calculateBase64Length(stream.GetSize()) + int64(len(afterContent))
beforeContentReader := strings.NewReader(beforeContent)
contentReader, contentWriter := io.Pipe()
go func() {
encoder := base64.NewEncoder(base64.StdEncoding, contentWriter)
if _, err := io.Copy(encoder, stream); err != nil {
_ = contentWriter.CloseWithError(err)
return
}
_ = encoder.Close()
_ = contentWriter.Close()
}()
afterContentReader := strings.NewReader(afterContent)
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
fmt.Sprintf("https://api.github.com/repos/%s/%s/git/blobs", d.Owner, d.Repo),
&ReaderWithProgress{
Reader: io.MultiReader(beforeContentReader, contentReader, afterContentReader),
Length: length,
Progress: up,
})
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("Authorization", "Bearer "+d.Token)
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
req.ContentLength = length
res, err := base.HttpClient.Do(req)
if err != nil {
return "", err
}
resBody, err := io.ReadAll(res.Body)
if err != nil {
return "", err
}
if res.StatusCode != 201 {
var errMsg ErrResp
if err = utils.Json.Unmarshal(resBody, &errMsg); err != nil {
return "", errors.New(res.Status)
} else {
return "", fmt.Errorf("%s: %s", res.Status, errMsg.Message)
}
}
var resp PutBlobResp
if err = utils.Json.Unmarshal(resBody, &resp); err != nil {
return "", err
}
return resp.Sha, nil
}
func (d *Github) delete(path, sha, message string) error {
body := map[string]interface{}{
"message": message,
"sha": sha,
"branch": d.Ref,
}
d.addCommitterAndAuthor(&body)
res, err := d.client.R().SetBody(body).Delete(d.getContentApiUrl(path))
if err != nil {
return err
}
if res.StatusCode() != 200 {
return toErr(res)
}
return nil
}
func (d *Github) renewParentTrees(path, prevSha, curSha, until string) (string, error) {
for path != until {
path = stdpath.Dir(path)
tree, sha, err := d.getTreeDirectly(path)
if err != nil {
return "", err
}
var newTree *TreeObjReq = nil
for _, t := range tree.Trees {
if t.Sha == prevSha {
newTree = &t.TreeObjReq
newTree.Sha = curSha
break
}
}
if newTree == nil {
return "", errs.ObjectNotFound
}
curSha, err = d.newTree(sha, []interface{}{*newTree})
if err != nil {
return "", err
}
prevSha = sha
}
return curSha, nil
}
func (d *Github) getTree(sha string) (*TreeResp, error) {
res, err := d.client.R().Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/git/trees/%s", d.Owner, d.Repo, sha))
if err != nil {
return nil, err
}
if res.StatusCode() != 200 {
return nil, toErr(res)
}
var resp TreeResp
if err = utils.Json.Unmarshal(res.Body(), &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (d *Github) getTreeDirectly(path string) (*TreeResp, string, error) {
p, err := d.get(path)
if err != nil {
return nil, "", err
}
if p.Entries == nil {
return nil, "", fmt.Errorf("%s is not a folder", path)
}
tree, err := d.getTree(p.Sha)
if err != nil {
return nil, "", err
}
if tree.Truncated {
return nil, "", fmt.Errorf("tree %s is truncated", path)
}
return tree, p.Sha, nil
}
func (d *Github) newTree(baseSha string, tree []interface{}) (string, error) {
res, err := d.client.R().
SetBody(&TreeReq{
BaseTree: baseSha,
Trees: tree,
}).
Post(fmt.Sprintf("https://api.github.com/repos/%s/%s/git/trees", d.Owner, d.Repo))
if err != nil {
return "", err
}
if res.StatusCode() != 201 {
return "", toErr(res)
}
var resp TreeResp
if err = utils.Json.Unmarshal(res.Body(), &resp); err != nil {
return "", err
}
return resp.Sha, nil
}
func (d *Github) commit(message, treeSha string) error {
oldCommit, err := d.getBranchHead()
body := map[string]interface{}{
"message": message,
"tree": treeSha,
"parents": []string{oldCommit},
}
d.addCommitterAndAuthor(&body)
res, err := d.client.R().SetBody(body).Post(fmt.Sprintf("https://api.github.com/repos/%s/%s/git/commits", d.Owner, d.Repo))
if err != nil {
return err
}
if res.StatusCode() != 201 {
return toErr(res)
}
var resp CommitResp
if err = utils.Json.Unmarshal(res.Body(), &resp); err != nil {
return err
}
// update branch head
res, err = d.client.R().
SetBody(&UpdateRefReq{
Sha: resp.Sha,
Force: false,
}).
Patch(fmt.Sprintf("https://api.github.com/repos/%s/%s/git/refs/heads/%s", d.Owner, d.Repo, d.Ref))
if err != nil {
return err
}
if res.StatusCode() != 200 {
return toErr(res)
}
return nil
}
func (d *Github) getBranchHead() (string, error) {
res, err := d.client.R().Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/branches/%s", d.Owner, d.Repo, d.Ref))
if err != nil {
return "", err
}
if res.StatusCode() != 200 {
return "", toErr(res)
}
var resp BranchResp
if err = utils.Json.Unmarshal(res.Body(), &resp); err != nil {
return "", err
}
return resp.Commit.Sha, nil
}
func (d *Github) copyWithoutRenewTree(srcObj, dstDir model.Obj) (dstSha, newSha, srcParentSha string, srcParentTree *TreeResp, err error) {
dst, err := d.get(dstDir.GetPath())
if err != nil {
return "", "", "", nil, err
}
if dst.Entries == nil {
return "", "", "", nil, errs.NotFolder
}
dstSha = dst.Sha
srcParentPath := stdpath.Dir(srcObj.GetPath())
srcParentTree, srcParentSha, err = d.getTreeDirectly(srcParentPath)
if err != nil {
return "", "", "", nil, err
}
var src *TreeObjReq = nil
for _, t := range srcParentTree.Trees {
if t.Path == srcObj.GetName() {
if t.Type == "commit" {
return "", "", "", nil, errors.New("cannot copy a submodule")
}
src = &t.TreeObjReq
break
}
}
if src == nil {
return "", "", "", nil, errs.ObjectNotFound
}
newTree := make([]interface{}, 0, 2)
newTree = append(newTree, *src)
if len(dst.Entries) == 1 && dst.Entries[0].Name == ".gitkeep" {
newTree = append(newTree, TreeObjReq{
Path: ".gitkeep",
Mode: "100644",
Type: "blob",
Sha: nil,
})
}
newSha, err = d.newTree(dstSha, newTree)
if err != nil {
return "", "", "", nil, err
}
return dstSha, newSha, srcParentSha, srcParentTree, nil
}
func (d *Github) getRepo() (*RepoResp, error) {
res, err := d.client.R().Get(fmt.Sprintf("https://api.github.com/repos/%s/%s", d.Owner, d.Repo))
if err != nil {
return nil, err
}
if res.StatusCode() != 200 {
return nil, toErr(res)
}
var resp RepoResp
if err = utils.Json.Unmarshal(res.Body(), &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (d *Github) addCommitterAndAuthor(m *map[string]interface{}) {
if d.CommitterName != "" {
committer := map[string]string{
"name": d.CommitterName,
"email": d.CommitterEmail,
}
(*m)["committer"] = committer
}
if d.AuthorName != "" {
author := map[string]string{
"name": d.AuthorName,
"email": d.AuthorEmail,
}
(*m)["author"] = author
}
}

36
drivers/github/meta.go Normal file
View File

@ -0,0 +1,36 @@
package github
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."`
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: "GitHub API",
LocalSort: true,
DefaultRoot: "/",
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Github{}
})
}

102
drivers/github/types.go Normal file
View File

@ -0,0 +1,102 @@
package github
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"`
}
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 string `json:"base_tree"`
Trees []interface{} `json:"tree"`
}
type CommitResp struct {
Sha string `json:"sha"`
}
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"`
}

115
drivers/github/util.go Normal file
View File

@ -0,0 +1,115 @@
package github
import (
"context"
"errors"
"fmt"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
"io"
"math"
"strings"
"text/template"
)
type ReaderWithProgress struct {
Reader io.Reader
Length int64
Progress func(percentage float64)
offset int64
}
func (r *ReaderWithProgress) Read(p []byte) (int, error) {
n, err := r.Reader.Read(p)
r.offset += int64(n)
r.Progress(math.Min(100.0, float64(r.offset)/float64(r.Length)*100.0))
return n, err
}
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 {
return errors.New(res.Status())
} else {
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
}