From 0d8bc7901a7c656adef9147d7be043539230bbd6 Mon Sep 17 00:00:00 2001 From: Sakana Date: Sun, 19 Jan 2025 18:50:13 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(github=5Freleases):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=AF=B9=20GitHub=20Releases=20=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/all.go | 1 + drivers/github_releases/driver.go | 119 +++++++++++++++++++++++++++++ drivers/github_releases/meta.go | 32 ++++++++ drivers/github_releases/types.go | 53 +++++++++++++ drivers/github_releases/util.go | 123 ++++++++++++++++++++++++++++++ 5 files changed, 328 insertions(+) create mode 100644 drivers/github_releases/driver.go create mode 100644 drivers/github_releases/meta.go create mode 100644 drivers/github_releases/types.go create mode 100644 drivers/github_releases/util.go diff --git a/drivers/all.go b/drivers/all.go index 8b253a08558..bd051168eaa 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -25,6 +25,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/febbox" _ "github.com/alist-org/alist/v3/drivers/ftp" _ "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_photo" _ "github.com/alist-org/alist/v3/drivers/halalcloud" diff --git a/drivers/github_releases/driver.go b/drivers/github_releases/driver.go new file mode 100644 index 00000000000..3ae6a28f038 --- /dev/null +++ b/drivers/github_releases/driver.go @@ -0,0 +1,119 @@ +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" +) + +type GithubReleases struct { + model.Storage + Addition + + repoList []Repo +} + +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 { + repos, err := ParseRepos(d.Addition.RepoStructure) + if err != nil { + return err + } + d.repoList = repos + return nil +} + +func (d *GithubReleases) Drop(ctx context.Context) error { + 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.repoList { + if repo.Path == path { // 与仓库路径相同 + tempFiles, err := RequestGithubReleases(repo.RepoName, path) + if err != nil { + return nil, err + } + files = append(files, tempFiles...) + if d.Addition.ShowReadme { + tempFiles, err = RequestGithubOtherFile(repo.RepoName, path) + if err != nil { + return nil, err + } + files = append(files, tempFiles...) + } + + } else if strings.HasPrefix(repo.Path, path) { // 仓库路径是目录的子目录 + nextDir := GetNextDir(repo.Path, path) + if nextDir == "" { + continue + } + files = append(files, File{ + FileName: nextDir, + Size: 0, + CreateAt: time.Time{}, + UpdateAt: time.Now(), + Url: "", + Type: "dir", + Path: fmt.Sprintf("%s/%s", path, nextDir), + }) + } + } + + objs := make([]model.Obj, len(files)) + for i, file := range files { + objs[i] = file + } + return objs, 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) diff --git a/drivers/github_releases/meta.go b/drivers/github_releases/meta.go new file mode 100644 index 00000000000..a9876657cbf --- /dev/null +++ b/drivers/github_releases/meta.go @@ -0,0 +1,32 @@ +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"` +} + +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{} + }) +} diff --git a/drivers/github_releases/types.go b/drivers/github_releases/types.go new file mode 100644 index 00000000000..cdcf185aacd --- /dev/null +++ b/drivers/github_releases/types.go @@ -0,0 +1,53 @@ +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 "" +} diff --git a/drivers/github_releases/util.go b/drivers/github_releases/util.go new file mode 100644 index 00000000000..210c5b7d23f --- /dev/null +++ b/drivers/github_releases/util.go @@ -0,0 +1,123 @@ +package github_releases + +import ( + "fmt" + "regexp" + "strings" + "time" + + "github.com/alist-org/alist/v3/drivers/base" + jsoniter "github.com/json-iterator/go" +) + +type Repo struct { + Path string + RepoName string +} + +// 解析仓库列表 +func ParseRepos(text string) ([]Repo, error) { + lines := strings.Split(text, "\n") + var repos []Repo + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.Split(line, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid format: %s", line) + } + repos = append(repos, Repo{ + Path: fmt.Sprintf("/%s", strings.Trim(parts[0], "/")), + RepoName: parts[1], + }) + } + return repos, nil +} + +// 获取 Github Releases +func RequestGithubReleases(repo string, basePath string) ([]File, error) { + req := base.RestyClient.R() + res, err := req.Get(fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", strings.Trim(repo, "/"))) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + return nil, fmt.Errorf("request failed: %s", res.Status()) + } + 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 files, nil +} + +// 获取 README、LICENSE 等文件 +func RequestGithubOtherFile(repo string, basePath string) ([]File, error) { + req := base.RestyClient.R() + res, err := req.Get(fmt.Sprintf("https://api.github.com/repos/%s/contents/", strings.Trim(repo, "/"))) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + return nil, fmt.Errorf("request failed: %s", res.Status()) + } + 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 +} + +// 获取下一级目录 +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 "" +} From ecd6f8b7ab36417ccb74009508e6da7a544ff996 Mon Sep 17 00:00:00 2001 From: Sakana Date: Tue, 21 Jan 2025 16:55:48 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(github=5Freleases):=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E7=9B=AE=E5=BD=95=E5=A4=A7=E5=B0=8F=E5=92=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=97=B6=E9=97=B4=EF=BC=8C=E5=A2=9E=E5=8A=A0=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/github_releases/driver.go | 55 +++++++---- drivers/github_releases/types.go | 13 +++ drivers/github_releases/util.go | 150 +++++++++++++++++++----------- 3 files changed, 144 insertions(+), 74 deletions(-) diff --git a/drivers/github_releases/driver.go b/drivers/github_releases/driver.go index 3ae6a28f038..32a116d2feb 100644 --- a/drivers/github_releases/driver.go +++ b/drivers/github_releases/driver.go @@ -11,6 +11,7 @@ import ( "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 { @@ -47,17 +48,18 @@ func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.Lis for _, repo := range d.repoList { if repo.Path == path { // 与仓库路径相同 - tempFiles, err := RequestGithubReleases(repo.RepoName, path) + resp, err := GetRepoReleaseInfo(repo.RepoName, path, d.Storage.CacheExpiration) if err != nil { return nil, err } - files = append(files, tempFiles...) + files = append(files, resp.Files...) + if d.Addition.ShowReadme { - tempFiles, err = RequestGithubOtherFile(repo.RepoName, path) + resp, err := GetGithubOtherFile(repo.RepoName, path, d.Storage.CacheExpiration) if err != nil { return nil, err } - files = append(files, tempFiles...) + files = append(files, *resp...) } } else if strings.HasPrefix(repo.Path, path) { // 仓库路径是目录的子目录 @@ -65,23 +67,40 @@ func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.Lis if nextDir == "" { continue } - files = append(files, File{ - FileName: nextDir, - Size: 0, - CreateAt: time.Time{}, - UpdateAt: time.Now(), - Url: "", - Type: "dir", - Path: fmt.Sprintf("%s/%s", path, nextDir), - }) + repo, _ := GetRepoReleaseInfo(repo.RepoName, 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), + }) + } } } - objs := make([]model.Obj, len(files)) - for i, file := range files { - objs[i] = file - } - return objs, nil + 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) { diff --git a/drivers/github_releases/types.go b/drivers/github_releases/types.go index cdcf185aacd..1b3f3533f72 100644 --- a/drivers/github_releases/types.go +++ b/drivers/github_releases/types.go @@ -51,3 +51,16 @@ func (f File) GetID() string { func (f File) Thumb() string { return "" } + +type GithubReleasesData struct { + Files []File `json:"files"` + Size int64 `json:"size"` + UpdateAt time.Time `json:"chtime"` + CreateAt time.Time `json:"time"` + Url string `json:"url"` +} + +type Repo struct { + Path string + RepoName string +} diff --git a/drivers/github_releases/util.go b/drivers/github_releases/util.go index 210c5b7d23f..31c11dc97a6 100644 --- a/drivers/github_releases/util.go +++ b/drivers/github_releases/util.go @@ -4,16 +4,19 @@ 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" ) -type Repo struct { - Path string - RepoName string -} +var ( + cache = make(map[string]*resty.Response) + created = make(map[string]time.Time) + mu sync.Mutex +) // 解析仓库列表 func ParseRepos(text string) ([]Repo, error) { @@ -36,52 +39,50 @@ func ParseRepos(text string) ([]Repo, error) { return repos, nil } -// 获取 Github Releases -func RequestGithubReleases(repo string, basePath string) ([]File, error) { - req := base.RestyClient.R() - res, err := req.Get(fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", strings.Trim(repo, "/"))) - if err != nil { - return nil, err +// 获取下一级目录 +func GetNextDir(wholePath string, basePath string) string { + if !strings.HasSuffix(basePath, "/") { + basePath += "/" } - if res.StatusCode() != 200 { - return nil, fmt.Errorf("request failed: %s", res.Status()) + if !strings.HasPrefix(wholePath, basePath) { + return "" } - assets := jsoniter.Get(res.Body(), "assets") - var files []File - - for i := 0; i < assets.Size(); i++ { - filename := assets.Get(i, "name").ToString() + remainingPath := strings.TrimLeft(strings.TrimPrefix(wholePath, basePath), "/") + if remainingPath != "" { + parts := strings.Split(remainingPath, "/") + return parts[0] + } + return "" +} - 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), +// 发送 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() - 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 - }(), - }) + res, err := base.RestyClient.R().Get(url) + if err != nil || res.StatusCode() != 200 { + return nil, fmt.Errorf("request fail: %v", err) } - return files, nil + + mu.Lock() + cache[url] = res + created[url] = time.Now() + mu.Unlock() + + return res, nil } // 获取 README、LICENSE 等文件 -func RequestGithubOtherFile(repo string, basePath string) ([]File, error) { - req := base.RestyClient.R() - res, err := req.Get(fmt.Sprintf("https://api.github.com/repos/%s/contents/", strings.Trim(repo, "/"))) - if err != nil { - return nil, err - } - if res.StatusCode() != 200 { - return nil, fmt.Errorf("request failed: %s", res.Status()) - } +func GetGithubOtherFile(repo string, basePath string, CacheExpiration int) (*[]File, error) { + res, _ := GetRequest( + fmt.Sprintf("https://api.github.com/repos/%s/contents/", strings.Trim(repo, "/")), + CacheExpiration, + ) body := jsoniter.Get(res.Body()) var files []File for i := 0; i < body.Size(); i++ { @@ -103,21 +104,58 @@ func RequestGithubOtherFile(repo string, basePath string) ([]File, error) { Path: fmt.Sprintf("%s/%s", basePath, filename), }) } - return files, nil + return &files, 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] +// 获取 GitHub Release 详细信息 +func GetRepoReleaseInfo(repo string, basePath string, CacheExpiration int) (*GithubReleasesData, error) { + res, _ := GetRequest( + fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", strings.Trim(repo, "/")), + CacheExpiration, + ) + body := res.Body() + 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 "" + + return &GithubReleasesData{ + 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 } From ce436f994483081e80e4e20cd0dc68d516c6933d Mon Sep 17 00:00:00 2001 From: Sakana Date: Tue, 21 Jan 2025 19:27:01 +0800 Subject: [PATCH 3/6] =?UTF-8?q?Feat(github=5Freleases):=20=E5=8F=AF?= =?UTF-8?q?=E9=80=89=E5=A1=AB=E5=85=A5=20GitHub=20token=20=E6=9D=A5?= =?UTF-8?q?=E6=8F=90=E9=AB=98=E9=80=9F=E7=8E=87=E9=99=90=E5=88=B6=E6=88=96?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E7=A7=81=E6=9C=89=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/github_releases/driver.go | 6 +++--- drivers/github_releases/meta.go | 1 + drivers/github_releases/util.go | 24 +++++++++++++++++------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/drivers/github_releases/driver.go b/drivers/github_releases/driver.go index 32a116d2feb..e5b00f4d9a8 100644 --- a/drivers/github_releases/driver.go +++ b/drivers/github_releases/driver.go @@ -48,14 +48,14 @@ func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.Lis for _, repo := range d.repoList { if repo.Path == path { // 与仓库路径相同 - resp, err := GetRepoReleaseInfo(repo.RepoName, path, d.Storage.CacheExpiration) + resp, err := GetRepoReleaseInfo(repo.RepoName, path, d.Storage.CacheExpiration, d.Addition.Token) if err != nil { return nil, err } files = append(files, resp.Files...) if d.Addition.ShowReadme { - resp, err := GetGithubOtherFile(repo.RepoName, path, d.Storage.CacheExpiration) + resp, err := GetGithubOtherFile(repo.RepoName, path, d.Storage.CacheExpiration, d.Addition.Token) if err != nil { return nil, err } @@ -67,7 +67,7 @@ func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.Lis if nextDir == "" { continue } - repo, _ := GetRepoReleaseInfo(repo.RepoName, path, d.Storage.CacheExpiration) + repo, _ := GetRepoReleaseInfo(repo.RepoName, path, d.Storage.CacheExpiration, d.Addition.Token) hasSameDir := false for index, file := range files { diff --git a/drivers/github_releases/meta.go b/drivers/github_releases/meta.go index a9876657cbf..9fff520a7f0 100644 --- a/drivers/github_releases/meta.go +++ b/drivers/github_releases/meta.go @@ -9,6 +9,7 @@ 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"` } var config = driver.Config{ diff --git a/drivers/github_releases/util.go b/drivers/github_releases/util.go index 31c11dc97a6..305af74cb4b 100644 --- a/drivers/github_releases/util.go +++ b/drivers/github_releases/util.go @@ -10,6 +10,7 @@ import ( "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 ( @@ -56,15 +57,22 @@ func GetNextDir(wholePath string, basePath string) string { } // 发送 GET 请求 -func GetRequest(url string, CacheExpiration int) (*resty.Response, error) { +func GetRequest(url string, cacheExpiration int, token string) (*resty.Response, error) { + log.SetLevel(log.DebugLevel) mu.Lock() - if res, ok := cache[url]; ok && time.Now().Before(created[url].Add(time.Duration(CacheExpiration)*time.Minute)) { + if res, ok := cache[url]; ok && time.Now().Before(created[url].Add(time.Duration(cacheExpiration)*time.Minute)) { mu.Unlock() + log.Debug("cache request", url) return res, nil } mu.Unlock() - res, err := base.RestyClient.R().Get(url) + req := base.RestyClient.R() + if token != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + } + res, err := req.Get(url) + log.Debug("nocache request", url) if err != nil || res.StatusCode() != 200 { return nil, fmt.Errorf("request fail: %v", err) } @@ -78,10 +86,11 @@ func GetRequest(url string, CacheExpiration int) (*resty.Response, error) { } // 获取 README、LICENSE 等文件 -func GetGithubOtherFile(repo string, basePath string, CacheExpiration int) (*[]File, error) { +func GetGithubOtherFile(repo string, basePath string, cacheExpiration int, token string) (*[]File, error) { res, _ := GetRequest( fmt.Sprintf("https://api.github.com/repos/%s/contents/", strings.Trim(repo, "/")), - CacheExpiration, + cacheExpiration, + token, ) body := jsoniter.Get(res.Body()) var files []File @@ -108,10 +117,11 @@ func GetGithubOtherFile(repo string, basePath string, CacheExpiration int) (*[]F } // 获取 GitHub Release 详细信息 -func GetRepoReleaseInfo(repo string, basePath string, CacheExpiration int) (*GithubReleasesData, error) { +func GetRepoReleaseInfo(repo string, basePath string, cacheExpiration int, token string) (*GithubReleasesData, error) { res, _ := GetRequest( fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", strings.Trim(repo, "/")), - CacheExpiration, + cacheExpiration, + token, ) body := res.Body() assets := jsoniter.Get(res.Body(), "assets") From c2a4e3482046c9ca894cf2b8c2668094cba4a4ab Mon Sep 17 00:00:00 2001 From: Sakana Date: Tue, 21 Jan 2025 23:50:04 +0800 Subject: [PATCH 4/6] =?UTF-8?q?Fix(github=5Freleases):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BB=93=E5=BA=93=E6=97=A0=E6=9D=83=E9=99=90=E6=88=96?= =?UTF-8?q?=E4=B8=8D=E5=AD=98=E5=9C=A8=E6=97=B6=E7=9A=84=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/github_releases/driver.go | 1 + drivers/github_releases/util.go | 33 ++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/drivers/github_releases/driver.go b/drivers/github_releases/driver.go index e5b00f4d9a8..c717b1dc9f8 100644 --- a/drivers/github_releases/driver.go +++ b/drivers/github_releases/driver.go @@ -39,6 +39,7 @@ func (d *GithubReleases) Init(ctx context.Context) error { } func (d *GithubReleases) Drop(ctx context.Context) error { + ClearCache() return nil } diff --git a/drivers/github_releases/util.go b/drivers/github_releases/util.go index 305af74cb4b..3dadc48dbe7 100644 --- a/drivers/github_releases/util.go +++ b/drivers/github_releases/util.go @@ -58,23 +58,25 @@ func GetNextDir(wholePath string, basePath string) string { // 发送 GET 请求 func GetRequest(url string, cacheExpiration int, token string) (*resty.Response, error) { - log.SetLevel(log.DebugLevel) mu.Lock() if res, ok := cache[url]; ok && time.Now().Before(created[url].Add(time.Duration(cacheExpiration)*time.Minute)) { mu.Unlock() - log.Debug("cache request", url) return res, nil } mu.Unlock() req := base.RestyClient.R() if token != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", 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") res, err := req.Get(url) - log.Debug("nocache request", url) - if err != nil || res.StatusCode() != 200 { - return nil, fmt.Errorf("request fail: %v", err) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + log.Warn("failed to get request: ", res.StatusCode(), res.String()) } mu.Lock() @@ -118,12 +120,14 @@ func GetGithubOtherFile(repo string, basePath string, cacheExpiration int, token // 获取 GitHub Release 详细信息 func GetRepoReleaseInfo(repo string, basePath string, cacheExpiration int, token string) (*GithubReleasesData, error) { - res, _ := GetRequest( - fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", strings.Trim(repo, "/")), - cacheExpiration, - token, - ) + url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", strings.Trim(repo, "/")) + res, _ := GetRequest(url, cacheExpiration, token) body := res.Body() + + if jsoniter.Get(res.Body(), "status").ToInt64() != 0 { + return &GithubReleasesData{}, fmt.Errorf("%s", res.String()) + } + assets := jsoniter.Get(res.Body(), "assets") var files []File @@ -169,3 +173,10 @@ func GetRepoReleaseInfo(repo string, basePath string, cacheExpiration int, token }(), }, nil } + +func ClearCache() { + mu.Lock() + cache = make(map[string]*resty.Response) + created = make(map[string]time.Time) + mu.Unlock() +} From 2b39e342a5c667459f0a04991860e6a3c69e9113 Mon Sep 17 00:00:00 2001 From: Sakana Date: Thu, 23 Jan 2025 12:58:04 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat(github=5Freleases):=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=98=BE=E7=A4=BA=E6=89=80=E6=9C=89=E7=89=88=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E5=BC=80=E5=90=AF=E5=90=8E=E4=B8=8D=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B9=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/github_releases/driver.go | 28 ++++++++--- drivers/github_releases/meta.go | 7 +-- drivers/github_releases/types.go | 10 ++-- drivers/github_releases/util.go | 78 +++++++++++++++++++++---------- 4 files changed, 85 insertions(+), 38 deletions(-) diff --git a/drivers/github_releases/driver.go b/drivers/github_releases/driver.go index c717b1dc9f8..79f2b582146 100644 --- a/drivers/github_releases/driver.go +++ b/drivers/github_releases/driver.go @@ -18,7 +18,7 @@ type GithubReleases struct { model.Storage Addition - repoList []Repo + releases []Release } func (d *GithubReleases) Config() driver.Config { @@ -30,11 +30,12 @@ func (d *GithubReleases) GetAddition() driver.Additional { } func (d *GithubReleases) Init(ctx context.Context) error { - repos, err := ParseRepos(d.Addition.RepoStructure) + SetHeader(d.Addition.Token) + repos, err := ParseRepos(d.Addition.RepoStructure, d.Addition.ShowAllVersion) if err != nil { return err } - d.repoList = repos + d.releases = repos return nil } @@ -47,16 +48,16 @@ func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.Lis files := make([]File, 0) path := fmt.Sprintf("/%s", strings.Trim(dir.GetPath(), "/")) - for _, repo := range d.repoList { + for _, repo := range d.releases { if repo.Path == path { // 与仓库路径相同 - resp, err := GetRepoReleaseInfo(repo.RepoName, path, d.Storage.CacheExpiration, d.Addition.Token) + 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, d.Addition.Token) + resp, err := GetGithubOtherFile(repo.RepoName, path, d.Storage.CacheExpiration) if err != nil { return nil, err } @@ -68,7 +69,20 @@ func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.Lis if nextDir == "" { continue } - repo, _ := GetRepoReleaseInfo(repo.RepoName, path, d.Storage.CacheExpiration, d.Addition.Token) + 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 { diff --git a/drivers/github_releases/meta.go b/drivers/github_releases/meta.go index 9fff520a7f0..ca6ca5dc8d0 100644 --- a/drivers/github_releases/meta.go +++ b/drivers/github_releases/meta.go @@ -7,9 +7,10 @@ import ( 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"` + 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{ diff --git a/drivers/github_releases/types.go b/drivers/github_releases/types.go index 1b3f3533f72..733460dca5f 100644 --- a/drivers/github_releases/types.go +++ b/drivers/github_releases/types.go @@ -52,7 +52,7 @@ func (f File) Thumb() string { return "" } -type GithubReleasesData struct { +type ReleasesData struct { Files []File `json:"files"` Size int64 `json:"size"` UpdateAt time.Time `json:"chtime"` @@ -60,7 +60,9 @@ type GithubReleasesData struct { Url string `json:"url"` } -type Repo struct { - Path string - RepoName string +type Release struct { + Path string // 挂载路径 + RepoName string // 仓库名称 + Version string // 版本号, tag + ID string // 版本ID } diff --git a/drivers/github_releases/util.go b/drivers/github_releases/util.go index 3dadc48dbe7..ecc2feaa8df 100644 --- a/drivers/github_releases/util.go +++ b/drivers/github_releases/util.go @@ -17,12 +17,13 @@ var ( cache = make(map[string]*resty.Response) created = make(map[string]time.Time) mu sync.Mutex + req *resty.Request ) // 解析仓库列表 -func ParseRepos(text string) ([]Repo, error) { +func ParseRepos(text string, allVersion bool) ([]Release, error) { lines := strings.Split(text, "\n") - var repos []Repo + var repos []Release for _, line := range lines { line = strings.TrimSpace(line) if line == "" { @@ -32,10 +33,21 @@ func ParseRepos(text string) ([]Repo, error) { if len(parts) != 2 { return nil, fmt.Errorf("invalid format: %s", line) } - repos = append(repos, Repo{ - Path: fmt.Sprintf("/%s", strings.Trim(parts[0], "/")), - RepoName: parts[1], - }) + + path := fmt.Sprintf("/%s", strings.Trim(parts[0], "/")) + + if allVersion { + releases, _ := GetAllVersion(parts[1], path) + repos = append(repos, *releases...) + } else { + repos = append(repos, Release{ + Path: path, + RepoName: parts[1], + Version: "latest", + ID: "latest", + }) + } + } return repos, nil } @@ -57,7 +69,7 @@ func GetNextDir(wholePath string, basePath string) string { } // 发送 GET 请求 -func GetRequest(url string, cacheExpiration int, token string) (*resty.Response, error) { +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() @@ -65,12 +77,6 @@ func GetRequest(url string, cacheExpiration int, token string) (*resty.Response, } mu.Unlock() - 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") res, err := req.Get(url) if err != nil { return nil, err @@ -88,12 +94,9 @@ func GetRequest(url string, cacheExpiration int, token string) (*resty.Response, } // 获取 README、LICENSE 等文件 -func GetGithubOtherFile(repo string, basePath string, cacheExpiration int, token string) (*[]File, error) { - res, _ := GetRequest( - fmt.Sprintf("https://api.github.com/repos/%s/contents/", strings.Trim(repo, "/")), - cacheExpiration, - token, - ) +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++ { @@ -119,13 +122,13 @@ func GetGithubOtherFile(repo string, basePath string, cacheExpiration int, token } // 获取 GitHub Release 详细信息 -func GetRepoReleaseInfo(repo string, basePath string, cacheExpiration int, token string) (*GithubReleasesData, error) { - url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", strings.Trim(repo, "/")) - res, _ := GetRequest(url, cacheExpiration, token) +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 &GithubReleasesData{}, fmt.Errorf("%s", res.String()) + return &ReleasesData{}, fmt.Errorf("%s", res.String()) } assets := jsoniter.Get(res.Body(), "assets") @@ -152,7 +155,7 @@ func GetRepoReleaseInfo(repo string, basePath string, cacheExpiration int, token }) } - return &GithubReleasesData{ + return &ReleasesData{ Files: files, Url: jsoniter.Get(body, "html_url").ToString(), @@ -174,9 +177,36 @@ func GetRepoReleaseInfo(repo string, basePath string, cacheExpiration int, token }, 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") +} From 97da5d2d2a2bd37084be9d50631e397be9f95d7e Mon Sep 17 00:00:00 2001 From: Sakana Date: Fri, 24 Jan 2025 21:44:18 +0800 Subject: [PATCH 6/6] =?UTF-8?q?feat(github=5Freleases):=20=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=97=A0=E5=AD=90=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/github_releases/util.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/drivers/github_releases/util.go b/drivers/github_releases/util.go index ecc2feaa8df..b2d79c0b3c1 100644 --- a/drivers/github_releases/util.go +++ b/drivers/github_releases/util.go @@ -30,19 +30,24 @@ func ParseRepos(text string, allVersion bool) ([]Release, error) { continue } parts := strings.Split(line, ":") - if len(parts) != 2 { + 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) } - path := fmt.Sprintf("/%s", strings.Trim(parts[0], "/")) - if allVersion { - releases, _ := GetAllVersion(parts[1], path) + releases, _ := GetAllVersion(repo, path) repos = append(repos, *releases...) } else { repos = append(repos, Release{ Path: path, - RepoName: parts[1], + RepoName: repo, Version: "latest", ID: "latest", })