mirror of https://github.com/Xhofe/alist
* feat(github_releases): 添加对 GitHub Releases 的支持 * feat(github_releases): 增加目录大小和更新时间,增加请求缓存 * Feat(github_releases): 可选填入 GitHub token 来提高速率限制或访问私有仓库 * Fix(github_releases): 修复仓库无权限或不存在时的异常 * feat(github_releases): 支持显示所有版本,开启后不显示文件夹大小 * feat(github_releases): 兼容无子目录pull/7847/head
parent
258b8f520f
commit
bdd9774aa7
|
@ -25,6 +25,7 @@ import (
|
||||||
_ "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/github"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/github_releases"
|
||||||
_ "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"
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
package github_releases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GithubReleases struct {
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
|
||||||
|
releases []Release
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) GetAddition() driver.Additional {
|
||||||
|
return &d.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) Init(ctx context.Context) error {
|
||||||
|
SetHeader(d.Addition.Token)
|
||||||
|
repos, err := ParseRepos(d.Addition.RepoStructure, d.Addition.ShowAllVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.releases = repos
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) Drop(ctx context.Context) error {
|
||||||
|
ClearCache()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
files := make([]File, 0)
|
||||||
|
path := fmt.Sprintf("/%s", strings.Trim(dir.GetPath(), "/"))
|
||||||
|
|
||||||
|
for _, repo := range d.releases {
|
||||||
|
if repo.Path == path { // 与仓库路径相同
|
||||||
|
resp, err := GetRepoReleaseInfo(repo.RepoName, repo.ID, path, d.Storage.CacheExpiration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files = append(files, resp.Files...)
|
||||||
|
|
||||||
|
if d.Addition.ShowReadme {
|
||||||
|
resp, err := GetGithubOtherFile(repo.RepoName, path, d.Storage.CacheExpiration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files = append(files, *resp...)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if strings.HasPrefix(repo.Path, path) { // 仓库路径是目录的子目录
|
||||||
|
nextDir := GetNextDir(repo.Path, path)
|
||||||
|
if nextDir == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if d.Addition.ShowAllVersion {
|
||||||
|
files = append(files, File{
|
||||||
|
FileName: nextDir,
|
||||||
|
Size: 0,
|
||||||
|
CreateAt: time.Time{},
|
||||||
|
UpdateAt: time.Time{},
|
||||||
|
Url: "",
|
||||||
|
Type: "dir",
|
||||||
|
Path: fmt.Sprintf("%s/%s", path, nextDir),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, _ := GetRepoReleaseInfo(repo.RepoName, repo.Version, path, d.Storage.CacheExpiration)
|
||||||
|
|
||||||
|
hasSameDir := false
|
||||||
|
for index, file := range files {
|
||||||
|
if file.FileName == nextDir {
|
||||||
|
hasSameDir = true
|
||||||
|
files[index].Size += repo.Size
|
||||||
|
files[index].UpdateAt = func(a time.Time, b time.Time) time.Time {
|
||||||
|
if a.After(b) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}(files[index].UpdateAt, repo.UpdateAt)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasSameDir {
|
||||||
|
files = append(files, File{
|
||||||
|
FileName: nextDir,
|
||||||
|
Size: repo.Size,
|
||||||
|
CreateAt: repo.CreateAt,
|
||||||
|
UpdateAt: repo.UpdateAt,
|
||||||
|
Url: repo.Url,
|
||||||
|
Type: "dir",
|
||||||
|
Path: fmt.Sprintf("%s/%s", path, nextDir),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||||
|
return src, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
link := model.Link{
|
||||||
|
URL: file.GetID(),
|
||||||
|
Header: http.Header{},
|
||||||
|
}
|
||||||
|
return &link, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
return errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GithubReleases) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*GithubReleases)(nil)
|
|
@ -0,0 +1,34 @@
|
||||||
|
package github_releases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Addition struct {
|
||||||
|
driver.RootID
|
||||||
|
RepoStructure string `json:"repo_structure" type:"text" required:"true" default:"/path/to/alist-gh:alistGo/alist\n/path/to2/alist-web-gh:AlistGo/alist-web" help:"structure:[path:]org/repo"`
|
||||||
|
ShowReadme bool `json:"show_readme" type:"bool" default:"true" help:"show README、LICENSE file"`
|
||||||
|
Token string `json:"token" type:"string" required:"false" help:"GitHub token, if you want to access private repositories or increase the rate limit"`
|
||||||
|
ShowAllVersion bool `json:"show_all_version" type:"bool" default:"false" help:"show all versions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "GitHub Releases",
|
||||||
|
LocalSort: false,
|
||||||
|
OnlyLocal: false,
|
||||||
|
OnlyProxy: false,
|
||||||
|
NoCache: false,
|
||||||
|
NoUpload: false,
|
||||||
|
NeedMs: false,
|
||||||
|
DefaultRoot: "",
|
||||||
|
CheckStatus: false,
|
||||||
|
Alert: "",
|
||||||
|
NoOverwriteUpload: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(func() driver.Driver {
|
||||||
|
return &GithubReleases{}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package github_releases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
FileName string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
CreateAt time.Time `json:"time"`
|
||||||
|
UpdateAt time.Time `json:"chtime"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) GetHash() utils.HashInfo {
|
||||||
|
return utils.HashInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) GetPath() string {
|
||||||
|
return f.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) GetSize() int64 {
|
||||||
|
return f.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) GetName() string {
|
||||||
|
return f.FileName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) ModTime() time.Time {
|
||||||
|
return f.UpdateAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) CreateTime() time.Time {
|
||||||
|
return f.CreateAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) IsDir() bool {
|
||||||
|
return f.Type == "dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) GetID() string {
|
||||||
|
return f.Url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) Thumb() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleasesData struct {
|
||||||
|
Files []File `json:"files"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
UpdateAt time.Time `json:"chtime"`
|
||||||
|
CreateAt time.Time `json:"time"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Release struct {
|
||||||
|
Path string // 挂载路径
|
||||||
|
RepoName string // 仓库名称
|
||||||
|
Version string // 版本号, tag
|
||||||
|
ID string // 版本ID
|
||||||
|
}
|
|
@ -0,0 +1,217 @@
|
||||||
|
package github_releases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cache = make(map[string]*resty.Response)
|
||||||
|
created = make(map[string]time.Time)
|
||||||
|
mu sync.Mutex
|
||||||
|
req *resty.Request
|
||||||
|
)
|
||||||
|
|
||||||
|
// 解析仓库列表
|
||||||
|
func ParseRepos(text string, allVersion bool) ([]Release, error) {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
var repos []Release
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Split(line, ":")
|
||||||
|
path, repo := "", ""
|
||||||
|
if len(parts) == 1 {
|
||||||
|
path = "/"
|
||||||
|
repo = parts[0]
|
||||||
|
} else if len(parts) == 2 {
|
||||||
|
path = fmt.Sprintf("/%s", strings.Trim(parts[0], "/"))
|
||||||
|
repo = parts[1]
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("invalid format: %s", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if allVersion {
|
||||||
|
releases, _ := GetAllVersion(repo, path)
|
||||||
|
repos = append(repos, *releases...)
|
||||||
|
} else {
|
||||||
|
repos = append(repos, Release{
|
||||||
|
Path: path,
|
||||||
|
RepoName: repo,
|
||||||
|
Version: "latest",
|
||||||
|
ID: "latest",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return repos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取下一级目录
|
||||||
|
func GetNextDir(wholePath string, basePath string) string {
|
||||||
|
if !strings.HasSuffix(basePath, "/") {
|
||||||
|
basePath += "/"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(wholePath, basePath) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
remainingPath := strings.TrimLeft(strings.TrimPrefix(wholePath, basePath), "/")
|
||||||
|
if remainingPath != "" {
|
||||||
|
parts := strings.Split(remainingPath, "/")
|
||||||
|
return parts[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送 GET 请求
|
||||||
|
func GetRequest(url string, cacheExpiration int) (*resty.Response, error) {
|
||||||
|
mu.Lock()
|
||||||
|
if res, ok := cache[url]; ok && time.Now().Before(created[url].Add(time.Duration(cacheExpiration)*time.Minute)) {
|
||||||
|
mu.Unlock()
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
res, err := req.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.StatusCode() != 200 {
|
||||||
|
log.Warn("failed to get request: ", res.StatusCode(), res.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
cache[url] = res
|
||||||
|
created[url] = time.Now()
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 README、LICENSE 等文件
|
||||||
|
func GetGithubOtherFile(repo string, basePath string, cacheExpiration int) (*[]File, error) {
|
||||||
|
url := fmt.Sprintf("https://api.github.com/repos/%s/contents/", strings.Trim(repo, "/"))
|
||||||
|
res, _ := GetRequest(url, cacheExpiration)
|
||||||
|
body := jsoniter.Get(res.Body())
|
||||||
|
var files []File
|
||||||
|
for i := 0; i < body.Size(); i++ {
|
||||||
|
filename := body.Get(i, "name").ToString()
|
||||||
|
|
||||||
|
re := regexp.MustCompile(`(?i)^(.*\.md|LICENSE)$`)
|
||||||
|
|
||||||
|
if !re.MatchString(filename) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
files = append(files, File{
|
||||||
|
FileName: filename,
|
||||||
|
Size: body.Get(i, "size").ToInt64(),
|
||||||
|
CreateAt: time.Time{},
|
||||||
|
UpdateAt: time.Now(),
|
||||||
|
Url: body.Get(i, "download_url").ToString(),
|
||||||
|
Type: body.Get(i, "type").ToString(),
|
||||||
|
Path: fmt.Sprintf("%s/%s", basePath, filename),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 GitHub Release 详细信息
|
||||||
|
func GetRepoReleaseInfo(repo string, version string, basePath string, cacheExpiration int) (*ReleasesData, error) {
|
||||||
|
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/%s", strings.Trim(repo, "/"), version)
|
||||||
|
res, _ := GetRequest(url, cacheExpiration)
|
||||||
|
body := res.Body()
|
||||||
|
|
||||||
|
if jsoniter.Get(res.Body(), "status").ToInt64() != 0 {
|
||||||
|
return &ReleasesData{}, fmt.Errorf("%s", res.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
assets := jsoniter.Get(res.Body(), "assets")
|
||||||
|
var files []File
|
||||||
|
|
||||||
|
for i := 0; i < assets.Size(); i++ {
|
||||||
|
filename := assets.Get(i, "name").ToString()
|
||||||
|
|
||||||
|
files = append(files, File{
|
||||||
|
FileName: filename,
|
||||||
|
Size: assets.Get(i, "size").ToInt64(),
|
||||||
|
Url: assets.Get(i, "browser_download_url").ToString(),
|
||||||
|
Type: assets.Get(i, "content_type").ToString(),
|
||||||
|
Path: fmt.Sprintf("%s/%s", basePath, filename),
|
||||||
|
|
||||||
|
CreateAt: func() time.Time {
|
||||||
|
t, _ := time.Parse(time.RFC3339, assets.Get(i, "created_at").ToString())
|
||||||
|
return t
|
||||||
|
}(),
|
||||||
|
UpdateAt: func() time.Time {
|
||||||
|
t, _ := time.Parse(time.RFC3339, assets.Get(i, "updated_at").ToString())
|
||||||
|
return t
|
||||||
|
}(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ReleasesData{
|
||||||
|
Files: files,
|
||||||
|
Url: jsoniter.Get(body, "html_url").ToString(),
|
||||||
|
|
||||||
|
Size: func() int64 {
|
||||||
|
size := int64(0)
|
||||||
|
for _, file := range files {
|
||||||
|
size += file.Size
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}(),
|
||||||
|
UpdateAt: func() time.Time {
|
||||||
|
t, _ := time.Parse(time.RFC3339, jsoniter.Get(body, "published_at").ToString())
|
||||||
|
return t
|
||||||
|
}(),
|
||||||
|
CreateAt: func() time.Time {
|
||||||
|
t, _ := time.Parse(time.RFC3339, jsoniter.Get(body, "created_at").ToString())
|
||||||
|
return t
|
||||||
|
}(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有的版本号
|
||||||
|
func GetAllVersion(repo string, path string) (*[]Release, error) {
|
||||||
|
url := fmt.Sprintf("https://api.github.com/repos/%s/releases", strings.Trim(repo, "/"))
|
||||||
|
res, _ := GetRequest(url, 0)
|
||||||
|
body := jsoniter.Get(res.Body())
|
||||||
|
releases := make([]Release, 0)
|
||||||
|
for i := 0; i < body.Size(); i++ {
|
||||||
|
version := body.Get(i, "tag_name").ToString()
|
||||||
|
releases = append(releases, Release{
|
||||||
|
Path: fmt.Sprintf("%s/%s", path, version),
|
||||||
|
Version: version,
|
||||||
|
RepoName: repo,
|
||||||
|
ID: body.Get(i, "id").ToString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &releases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearCache() {
|
||||||
|
mu.Lock()
|
||||||
|
cache = make(map[string]*resty.Response)
|
||||||
|
created = make(map[string]time.Time)
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetHeader(token string) {
|
||||||
|
req = base.RestyClient.R()
|
||||||
|
if token != "" {
|
||||||
|
req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
}
|
||||||
|
req.SetHeader("Accept", "application/vnd.github+json")
|
||||||
|
req.SetHeader("X-GitHub-Api-Version", "2022-11-28")
|
||||||
|
}
|
Loading…
Reference in New Issue