mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 13:03:09 +08:00
Merge branch 'main' into renovate/gopkg.in-ldap.v3-3.x
This commit is contained in:
@@ -115,7 +115,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version: "1.26.4"
|
||||
go-version: "1.27.0"
|
||||
|
||||
- name: Setup web
|
||||
run: bash build.sh dev web
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version: "1.26.4"
|
||||
go-version: "1.27.0"
|
||||
|
||||
- name: Setup web
|
||||
run: bash build.sh dev web
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version: '1.26.4'
|
||||
go-version: '1.27.0'
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version: '1.26'
|
||||
go-version: '1.27'
|
||||
|
||||
- name: Cache Musl
|
||||
id: cache-musl
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version: '1.26.4'
|
||||
go-version: '1.27.0'
|
||||
|
||||
- name: Cache Musl
|
||||
id: cache-musl
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version: '1.26.4'
|
||||
go-version: '1.27.0'
|
||||
|
||||
- name: Cache Musl
|
||||
id: cache-musl
|
||||
|
||||
+13
-3
@@ -184,6 +184,7 @@ func (y *Cloud189TV) getFiles(ctx context.Context, fileId string, isFamily bool)
|
||||
}
|
||||
fullUrl += "/listFiles.action"
|
||||
|
||||
pageSize := 130 // 每一页返回的文件数量
|
||||
res := make([]model.Obj, 0, 130)
|
||||
for pageNum := 1; ; pageNum++ {
|
||||
var resp Cloud189FilesResp
|
||||
@@ -195,7 +196,7 @@ func (y *Cloud189TV) getFiles(ctx context.Context, fileId string, isFamily bool)
|
||||
"mediaAttr": "0",
|
||||
"iconOption": "5",
|
||||
"pageNum": fmt.Sprint(pageNum),
|
||||
"pageSize": "130",
|
||||
"pageSize": fmt.Sprint(pageSize),
|
||||
})
|
||||
if isFamily {
|
||||
r.SetQueryParams(map[string]string{
|
||||
@@ -218,13 +219,22 @@ func (y *Cloud189TV) getFiles(ctx context.Context, fileId string, isFamily bool)
|
||||
if resp.FileListAO.Count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
FolderCount := len(resp.FileListAO.FolderList) // 当前文件夹总数
|
||||
FileCount := len(resp.FileListAO.FileList) // 当前文件总数
|
||||
PageCount := FolderCount + FileCount // 当前页数总数
|
||||
|
||||
for i := 0; i < len(resp.FileListAO.FolderList); i++ {
|
||||
for i := 0; i < FolderCount; i++ {
|
||||
res = append(res, &resp.FileListAO.FolderList[i])
|
||||
}
|
||||
for i := 0; i < len(resp.FileListAO.FileList); i++ {
|
||||
for i := 0; i < FileCount; i++ {
|
||||
res = append(res, &resp.FileListAO.FileList[i])
|
||||
}
|
||||
|
||||
// 文件数量小于设定数量时跳出
|
||||
if PageCount < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -191,6 +191,10 @@ type Cloud189File struct {
|
||||
// StarLabel int64 `json:"starLabel"`
|
||||
}
|
||||
|
||||
func normalizeCloud189Name(name string) string {
|
||||
return strings.ReplaceAll(name, "\\'", "'")
|
||||
}
|
||||
|
||||
func (c *Cloud189File) CreateTime() time.Time {
|
||||
return time.Time(c.CreateDate)
|
||||
}
|
||||
@@ -207,6 +211,8 @@ func (c *Cloud189File) GetID() string { return string(c.ID) }
|
||||
func (c *Cloud189File) GetPath() string { return "" }
|
||||
func (c *Cloud189File) Thumb() string { return c.Icon.SmallUrl }
|
||||
|
||||
func (c *Cloud189File) GetDisplayName() string { return normalizeCloud189Name(c.Name) }
|
||||
|
||||
// 文件夹
|
||||
type Cloud189Folder struct {
|
||||
ID String `json:"id"`
|
||||
@@ -238,6 +244,8 @@ func (c *Cloud189Folder) IsDir() bool { return true }
|
||||
func (c *Cloud189Folder) GetID() string { return string(c.ID) }
|
||||
func (c *Cloud189Folder) GetPath() string { return "" }
|
||||
|
||||
func (c *Cloud189Folder) GetDisplayName() string { return normalizeCloud189Name(c.Name) }
|
||||
|
||||
type Cloud189FilesResp struct {
|
||||
//ResCode int `json:"res_code"`
|
||||
//ResMessage string `json:"res_message"`
|
||||
|
||||
+13
-3
@@ -195,9 +195,10 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
|
||||
}
|
||||
|
||||
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
|
||||
pageSize := 1000 // 每一页返回的文件数量
|
||||
res := make([]model.Obj, 0, 100)
|
||||
for pageNum := 1; ; pageNum++ {
|
||||
resp, err := y.getFilesWithPage(ctx, fileId, isFamily, pageNum, 1000, y.OrderBy, y.OrderDirection)
|
||||
resp, err := y.getFilesWithPage(ctx, fileId, isFamily, pageNum, pageSize, y.OrderBy, y.OrderDirection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -205,14 +206,23 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool)
|
||||
if resp.FileListAO.Count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
FolderCount := len(resp.FileListAO.FolderList) // 当前文件夹总数
|
||||
FileCount := len(resp.FileListAO.FileList) // 当前文件总数
|
||||
PageCount := FolderCount + FileCount // 当前页数总数
|
||||
|
||||
for i := 0; i < len(resp.FileListAO.FolderList); i++ {
|
||||
for i := 0; i < FolderCount; i++ {
|
||||
res = append(res, &resp.FileListAO.FolderList[i])
|
||||
}
|
||||
for i := 0; i < len(resp.FileListAO.FileList); i++ {
|
||||
for i := 0; i < FileCount; i++ {
|
||||
resp.FileListAO.FileList[i].ParentID = fileId
|
||||
res = append(res, &resp.FileListAO.FileList[i])
|
||||
}
|
||||
|
||||
// 当前文件数量小于设定数量则跳出
|
||||
if PageCount < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -154,15 +154,17 @@ func (d *Github) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
ret := make([]model.Obj, 0, len(obj.Entries))
|
||||
for _, entry := range obj.Entries {
|
||||
if entry.Name == ".gitkeep" {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, entry.toModelObj())
|
||||
}
|
||||
d.fetchAccurateModifiedTimes(ctx, dir.GetPath(), ret)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (d *Github) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
|
||||
+18
-17
@@ -7,23 +7,24 @@ import (
|
||||
|
||||
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."`
|
||||
GitHubProxy string `json:"gh_proxy" type:"string" help:"GitHub proxy, e.g. https://ghproxy.net/raw.githubusercontent.com or https://gh-proxy.com/raw.githubusercontent.com"`
|
||||
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}}"`
|
||||
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."`
|
||||
AccurateModifiedTime bool `json:"accurate_modified_time" type:"bool" default:"false" help:"Best-effort accurate modified time for small directory listings. Default disabled. Adds one GitHub GraphQL request and falls back to legacy zero-time values on failure."`
|
||||
GitHubProxy string `json:"gh_proxy" type:"string" help:"GitHub proxy, e.g. https://ghproxy.net/raw.githubusercontent.com or https://gh-proxy.com/raw.githubusercontent.com"`
|
||||
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{
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
mtimeMaxEntries = 200
|
||||
githubGraphQLEndpoint = "https://api.github.com/graphql"
|
||||
)
|
||||
|
||||
var githubZeroTime = time.Unix(0, 0)
|
||||
|
||||
type graphQLHistory struct {
|
||||
Nodes []struct {
|
||||
CommittedDate time.Time `json:"committedDate"`
|
||||
} `json:"nodes"`
|
||||
}
|
||||
|
||||
type graphQLMtimeResponse struct {
|
||||
Data struct {
|
||||
Repository struct {
|
||||
Commit map[string]graphQLHistory `json:"commit"`
|
||||
} `json:"repository"`
|
||||
} `json:"data"`
|
||||
Errors []struct{} `json:"errors"`
|
||||
}
|
||||
|
||||
func quoteGraphQLString(value string) string {
|
||||
quoted, _ := json.Marshal(value)
|
||||
return string(quoted)
|
||||
}
|
||||
|
||||
func buildMtimeQuery(owner, repo, ref string, objs []model.Obj) string {
|
||||
histories := make([]string, 0, len(objs))
|
||||
for i, obj := range objs {
|
||||
path := strings.TrimPrefix(obj.GetPath(), "/")
|
||||
histories = append(histories, fmt.Sprintf(`p%d: history(first: 1, path: %s) { nodes { committedDate } }`, i, quoteGraphQLString(path)))
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`query {
|
||||
repository(owner: %s, name: %s) {
|
||||
commit: object(expression: %s) {
|
||||
... on Commit {
|
||||
%s
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
quoteGraphQLString(owner),
|
||||
quoteGraphQLString(repo),
|
||||
quoteGraphQLString(ref+"^{commit}"),
|
||||
strings.Join(histories, "\n\t\t\t\t"),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *Github) fetchAccurateModifiedTimes(ctx context.Context, dirPath string, objs []model.Obj) {
|
||||
token := strings.TrimSpace(d.Token)
|
||||
if !d.AccurateModifiedTime || token == "" || len(objs) == 0 || len(objs) > mtimeMaxEntries {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := d.client.R().
|
||||
SetContext(ctx).
|
||||
SetHeader("Accept", "application/vnd.github+json").
|
||||
SetHeader("Authorization", "Bearer "+token).
|
||||
SetBody(map[string]string{"query": buildMtimeQuery(d.Owner, d.Repo, d.Ref, objs)}).
|
||||
Post(githubGraphQLEndpoint)
|
||||
if err != nil {
|
||||
log.WithError(err).Warnf("github accurate mtime failed for %s: transport", dirPath)
|
||||
return
|
||||
}
|
||||
if res.StatusCode() != http.StatusOK {
|
||||
log.Warnf("github accurate mtime failed for %s: http_%d", dirPath, res.StatusCode())
|
||||
return
|
||||
}
|
||||
|
||||
var response graphQLMtimeResponse
|
||||
if err := utils.Json.Unmarshal(res.Body(), &response); err != nil {
|
||||
log.WithError(err).Warnf("github accurate mtime failed for %s: graphql", dirPath)
|
||||
return
|
||||
}
|
||||
if len(response.Errors) > 0 || response.Data.Repository.Commit == nil {
|
||||
log.Warnf("github accurate mtime failed for %s: graphql", dirPath)
|
||||
return
|
||||
}
|
||||
|
||||
for i, obj := range objs {
|
||||
history := response.Data.Repository.Commit[fmt.Sprintf("p%d", i)]
|
||||
if len(history.Nodes) == 0 {
|
||||
continue
|
||||
}
|
||||
if raw, ok := obj.(*model.Object); ok {
|
||||
raw.Modified = history.Nodes[0].CommittedDate
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
odriver "github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func TestDriverInfoIncludesAccurateModifiedTimeDefault(t *testing.T) {
|
||||
info := op.GetDriverInfoMap()["GitHub API"]
|
||||
for _, item := range info.Additional {
|
||||
if item.Name != "accurate_modified_time" {
|
||||
continue
|
||||
}
|
||||
if item.Default != "false" {
|
||||
t.Fatalf("unexpected default: %q", item.Default)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatal("accurate_modified_time item not registered")
|
||||
}
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return f(r)
|
||||
}
|
||||
|
||||
func newGithubTestDriver(rt roundTripFunc, token string, enabled bool) *Github {
|
||||
return &Github{
|
||||
Storage: model.Storage{MountPath: "/github-test", CacheExpiration: 10},
|
||||
Addition: Addition{
|
||||
RootPath: odriver.RootPath{RootFolderPath: "/"},
|
||||
Token: token,
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Ref: "main",
|
||||
AccurateModifiedTime: enabled,
|
||||
},
|
||||
client: resty.New().SetTransport(rt),
|
||||
}
|
||||
}
|
||||
|
||||
func newJSONResponse(status int, body string) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: status,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(t *testing.T, value any) string {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal json: %v", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func graphQLQueryFromRequest(t *testing.T, r *http.Request) string {
|
||||
t.Helper()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read graphql request body: %v", err)
|
||||
}
|
||||
var payload map[string]string
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
t.Fatalf("decode graphql request body: %v", err)
|
||||
}
|
||||
query := payload["query"]
|
||||
if query == "" {
|
||||
t.Fatalf("graphql request missing query: %s", string(body))
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
func newContentsPayload(t *testing.T, entries []Object) string {
|
||||
t.Helper()
|
||||
return mustJSON(t, map[string]any{
|
||||
"type": "dir",
|
||||
"sha": "tree-sha",
|
||||
"entries": entries,
|
||||
})
|
||||
}
|
||||
|
||||
func newTreePayload(t *testing.T, sha string, trees []TreeObjResp) string {
|
||||
t.Helper()
|
||||
return mustJSON(t, map[string]any{
|
||||
"sha": sha,
|
||||
"truncated": false,
|
||||
"tree": trees,
|
||||
})
|
||||
}
|
||||
|
||||
func newCommitGraphQLPayload(t *testing.T, histories map[string][]string) string {
|
||||
t.Helper()
|
||||
commit := make(map[string]any, len(histories))
|
||||
for alias, dates := range histories {
|
||||
nodes := make([]map[string]string, 0, len(dates))
|
||||
for _, date := range dates {
|
||||
nodes = append(nodes, map[string]string{"committedDate": date})
|
||||
}
|
||||
commit[alias] = map[string]any{"nodes": nodes}
|
||||
}
|
||||
return mustJSON(t, map[string]any{
|
||||
"data": map[string]any{
|
||||
"repository": map[string]any{"commit": commit},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func newSequentialEntries(count int) []Object {
|
||||
entries := make([]Object, 0, count)
|
||||
for i := range count {
|
||||
name := fmt.Sprintf("%03d.md", i)
|
||||
entries = append(entries, Object{Name: name, Path: "docs/" + name, Type: "file", Size: 1})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func mustObject(t *testing.T, obj model.Obj) *model.Object {
|
||||
t.Helper()
|
||||
raw, ok := model.UnwrapObjName(obj).(*model.Object)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected obj type %T", obj)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func TestListAppliesAccurateModifiedTimeInOneRequest(t *testing.T) {
|
||||
stamp := time.Date(2025, 12, 22, 4, 52, 41, 0, time.UTC)
|
||||
entries := []Object{
|
||||
{Name: "a.md", Path: "docs/a.md", Type: "file", Size: 1},
|
||||
{Name: ".gitkeep", Path: "docs/.gitkeep", Type: "file"},
|
||||
{Name: `quote " 文.md`, Path: `docs/quote " 文.md`, Type: "file", Size: 1},
|
||||
{Name: "control.md", Path: "docs/control\x01.md", Type: "file", Size: 1},
|
||||
}
|
||||
graphqlCalls := 0
|
||||
var query string
|
||||
drv := newGithubTestDriver(roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/contents/"):
|
||||
return newJSONResponse(http.StatusOK, newContentsPayload(t, entries)), nil
|
||||
case r.Method == http.MethodPost && r.URL.String() == githubGraphQLEndpoint:
|
||||
graphqlCalls++
|
||||
query = graphQLQueryFromRequest(t, r)
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer token" {
|
||||
t.Fatalf("unexpected authorization header: %q", got)
|
||||
}
|
||||
return newJSONResponse(http.StatusOK, newCommitGraphQLPayload(t, map[string][]string{
|
||||
"p0": {stamp.Format(time.RFC3339)},
|
||||
"p1": {},
|
||||
"p2": {},
|
||||
})), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}), "token", true)
|
||||
|
||||
objs, err := drv.List(context.Background(), &model.Object{Path: "/docs", Name: "docs", IsFolder: true}, model.ListArgs{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if graphqlCalls != 1 {
|
||||
t.Fatalf("expected one GraphQL request, got %d", graphqlCalls)
|
||||
}
|
||||
if strings.Count(query, "history(first: 1") != 3 ||
|
||||
!strings.Contains(query, `object(expression: "main^{commit}")`) ||
|
||||
!strings.Contains(query, `p0: history(first: 1, path: "docs/a.md")`) ||
|
||||
!strings.Contains(query, `p1: history(first: 1, path: "docs/quote \" 文.md")`) ||
|
||||
!strings.Contains(query, `p2: history(first: 1, path: "docs/control\u0001.md")`) {
|
||||
t.Fatalf("query should peel the ref and contain all listed paths once:\n%s", query)
|
||||
}
|
||||
if len(objs) != 3 {
|
||||
t.Fatalf("expected three objects after .gitkeep filtering, got %d", len(objs))
|
||||
}
|
||||
if first := mustObject(t, objs[0]); !first.ModTime().Equal(stamp) || !first.CreateTime().Equal(githubZeroTime) {
|
||||
t.Fatalf("unexpected first timestamps: mod=%v create=%v", first.ModTime(), first.CreateTime())
|
||||
}
|
||||
if second := mustObject(t, objs[1]); !second.ModTime().Equal(githubZeroTime) || !second.CreateTime().Equal(githubZeroTime) {
|
||||
t.Fatalf("unmatched entry should retain legacy timestamps: mod=%v create=%v", second.ModTime(), second.CreateTime())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSkipsAccurateModifiedTime(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enabled bool
|
||||
token string
|
||||
entries int
|
||||
}{
|
||||
{name: "disabled", enabled: false, token: "token", entries: 1},
|
||||
{name: "missing token", enabled: true, token: "", entries: 1},
|
||||
{name: "over entry limit", enabled: true, token: "token", entries: 201},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
graphqlCalls := 0
|
||||
payload := newContentsPayload(t, newSequentialEntries(tc.entries))
|
||||
drv := newGithubTestDriver(roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/contents/"):
|
||||
return newJSONResponse(http.StatusOK, payload), nil
|
||||
case r.Method == http.MethodPost && r.URL.String() == githubGraphQLEndpoint:
|
||||
graphqlCalls++
|
||||
return newJSONResponse(http.StatusOK, newCommitGraphQLPayload(t, nil)), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}), tc.token, tc.enabled)
|
||||
|
||||
objs, err := drv.List(context.Background(), &model.Object{Path: "/docs", Name: "docs", IsFolder: true}, model.ListArgs{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if graphqlCalls != 0 {
|
||||
t.Fatalf("expected zero GraphQL requests, got %d", graphqlCalls)
|
||||
}
|
||||
for _, obj := range objs {
|
||||
raw := mustObject(t, obj)
|
||||
if !raw.ModTime().Equal(githubZeroTime) || !raw.CreateTime().Equal(githubZeroTime) {
|
||||
t.Fatalf("legacy timestamps should be preserved: mod=%v create=%v", raw.ModTime(), raw.CreateTime())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListFallsBackWhenGraphQLFails(t *testing.T) {
|
||||
graphqlCalls := 0
|
||||
drv := newGithubTestDriver(roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/contents/"):
|
||||
return newJSONResponse(http.StatusOK, newContentsPayload(t, newSequentialEntries(1))), nil
|
||||
case r.Method == http.MethodPost && r.URL.String() == githubGraphQLEndpoint:
|
||||
graphqlCalls++
|
||||
return newJSONResponse(http.StatusOK, `{"errors":[{"message":"rate limited"}]}`), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}), "token", true)
|
||||
|
||||
objs, err := drv.List(context.Background(), &model.Object{Path: "/docs", Name: "docs", IsFolder: true}, model.ListArgs{})
|
||||
if err != nil {
|
||||
t.Fatalf("GraphQL failure should be best-effort: %v", err)
|
||||
}
|
||||
if graphqlCalls != 1 {
|
||||
t.Fatalf("expected one GraphQL request, got %d", graphqlCalls)
|
||||
}
|
||||
obj := mustObject(t, objs[0])
|
||||
if !obj.ModTime().Equal(githubZeroTime) || !obj.CreateTime().Equal(githubZeroTime) {
|
||||
t.Fatalf("failed GraphQL request should retain legacy timestamps: mod=%v create=%v", obj.ModTime(), obj.CreateTime())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUsesOneGraphQLRequestAtEntryLimit(t *testing.T) {
|
||||
stamp := time.Date(2025, 12, 22, 4, 52, 41, 0, time.UTC)
|
||||
graphqlCalls := 0
|
||||
var query string
|
||||
drv := newGithubTestDriver(roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/contents/"):
|
||||
return newJSONResponse(http.StatusOK, newContentsPayload(t, newSequentialEntries(200))), nil
|
||||
case r.Method == http.MethodPost && r.URL.String() == githubGraphQLEndpoint:
|
||||
graphqlCalls++
|
||||
query = graphQLQueryFromRequest(t, r)
|
||||
return newJSONResponse(http.StatusOK, newCommitGraphQLPayload(t, map[string][]string{
|
||||
"p199": {stamp.Format(time.RFC3339)},
|
||||
})), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}), "token", true)
|
||||
|
||||
objs, err := drv.List(context.Background(), &model.Object{Path: "/docs", Name: "docs", IsFolder: true}, model.ListArgs{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if graphqlCalls != 1 || strings.Count(query, "history(first: 1") != 200 {
|
||||
t.Fatalf("200 entries should share one request: calls=%d histories=%d", graphqlCalls, strings.Count(query, "history(first: 1"))
|
||||
}
|
||||
if !strings.Contains(query, `p199: history(first: 1, path: "docs/199.md")`) {
|
||||
t.Fatalf("query missing final entry:\n%s", query)
|
||||
}
|
||||
if len(objs) != 200 || !mustObject(t, objs[199]).ModTime().Equal(stamp) {
|
||||
t.Fatalf("unexpected final object timestamp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListKeepsTreeFallbackOnLegacyPath(t *testing.T) {
|
||||
entries := make([]Object, 0, 1000)
|
||||
for i := range 1000 {
|
||||
name := fmt.Sprintf("dir-%d", i)
|
||||
entries = append(entries, Object{Name: name, Path: "docs/" + name, Type: "dir"})
|
||||
}
|
||||
graphqlCalls := 0
|
||||
drv := newGithubTestDriver(roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/contents/"):
|
||||
return newJSONResponse(http.StatusOK, newContentsPayload(t, entries)), nil
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/git/trees/"):
|
||||
return newJSONResponse(http.StatusOK, newTreePayload(t, "tree-sha", []TreeObjResp{{TreeObjReq: TreeObjReq{Path: "child.md", Mode: "100644", Type: "blob", Sha: "blob-sha"}, Size: 1, URL: "https://example.invalid/blob"}})), nil
|
||||
case r.Method == http.MethodPost && r.URL.String() == githubGraphQLEndpoint:
|
||||
graphqlCalls++
|
||||
return newJSONResponse(http.StatusOK, newCommitGraphQLPayload(t, nil)), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}), "token", true)
|
||||
|
||||
objs, err := drv.List(context.Background(), &model.Object{Path: "/docs", Name: "docs", IsFolder: true}, model.ListArgs{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(objs) != 1 {
|
||||
t.Fatalf("unexpected tree fallback result length: %d", len(objs))
|
||||
}
|
||||
first := mustObject(t, objs[0])
|
||||
if first.GetPath() != "/child.md" {
|
||||
t.Fatalf("unexpected tree fallback path: %s", first.GetPath())
|
||||
}
|
||||
if !first.ModTime().Equal(githubZeroTime) || !first.CreateTime().Equal(githubZeroTime) {
|
||||
t.Fatalf("tree fallback should preserve legacy timestamps: mod=%v create=%v", first.ModTime(), first.CreateTime())
|
||||
}
|
||||
if graphqlCalls != 0 {
|
||||
t.Fatalf("tree fallback should skip GraphQL, got %d calls", graphqlCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpListCacheHitDoesNotRepeatGraphQL(t *testing.T) {
|
||||
op.Cache.ClearAll()
|
||||
defer op.Cache.ClearAll()
|
||||
stamp := time.Date(2025, 12, 22, 4, 52, 41, 0, time.UTC)
|
||||
graphqlCalls := 0
|
||||
drv := newGithubTestDriver(roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/contents/"):
|
||||
return newJSONResponse(http.StatusOK, newContentsPayload(t, []Object{{Name: "a.md", Path: "a.md", Type: "file", Size: 1}})), nil
|
||||
case r.Method == http.MethodPost && r.URL.String() == githubGraphQLEndpoint:
|
||||
graphqlCalls++
|
||||
return newJSONResponse(http.StatusOK, newCommitGraphQLPayload(t, map[string][]string{"p0": {stamp.Format(time.RFC3339)}})), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}), "token", true)
|
||||
|
||||
first, err := op.List(context.Background(), drv, "/", model.ListArgs{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected first list error: %v", err)
|
||||
}
|
||||
second, err := op.List(context.Background(), drv, "/", model.ListArgs{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected second list error: %v", err)
|
||||
}
|
||||
if len(first) != 1 || len(second) != 1 {
|
||||
t.Fatalf("unexpected cached results: first=%d second=%d", len(first), len(second))
|
||||
}
|
||||
if graphqlCalls != 1 {
|
||||
t.Fatalf("expected one GraphQL call across cached lists, got %d", graphqlCalls)
|
||||
}
|
||||
if !mustObject(t, first[0]).ModTime().Equal(stamp) {
|
||||
t.Fatalf("expected first list to include backfilled modified time, got %v", mustObject(t, first[0]).ModTime())
|
||||
}
|
||||
if !mustObject(t, second[0]).ModTime().Equal(stamp) {
|
||||
t.Fatalf("expected cached list to retain modified time, got %v", mustObject(t, second[0]).ModTime())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
)
|
||||
@@ -35,7 +33,8 @@ func (o *Object) toModelObj() *model.Object {
|
||||
return &model.Object{
|
||||
Name: o.Name,
|
||||
Size: o.Size,
|
||||
Modified: time.Unix(0, 0),
|
||||
Modified: githubZeroTime,
|
||||
Ctime: githubZeroTime,
|
||||
IsFolder: o.Type == "dir",
|
||||
Path: utils.FixAndCleanPath(o.Path),
|
||||
}
|
||||
@@ -69,7 +68,8 @@ func (o *TreeObjResp) toModelObj() *model.Object {
|
||||
return &model.Object{
|
||||
Name: o.Path,
|
||||
Size: o.Size,
|
||||
Modified: time.Unix(0, 0),
|
||||
Modified: githubZeroTime,
|
||||
Ctime: githubZeroTime,
|
||||
IsFolder: o.Type == "tree",
|
||||
Path: utils.FixAndCleanPath(o.Path),
|
||||
}
|
||||
|
||||
@@ -241,6 +241,7 @@ func (d *GuangYaPan) multipartUploadToOSS(ctx context.Context, bucket *oss.Bucke
|
||||
}
|
||||
|
||||
parts := make([]oss.UploadPart, 0, partCount)
|
||||
var uploaded int64
|
||||
for i := 0; i < partCount; i++ {
|
||||
if utils.IsCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
@@ -273,6 +274,10 @@ func (d *GuangYaPan) multipartUploadToOSS(ctx context.Context, bucket *oss.Bucke
|
||||
return fmt.Errorf("failed to upload part %d: %w", i+1, err)
|
||||
}
|
||||
parts = append(parts, part)
|
||||
uploaded += length
|
||||
if total > 0 {
|
||||
up(100 * float64(uploaded) / float64(total))
|
||||
}
|
||||
}
|
||||
|
||||
_, err = bucket.CompleteMultipartUpload(imur, parts)
|
||||
|
||||
@@ -2,10 +2,10 @@ module github.com/OpenListTeam/OpenList/v4
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.26.4
|
||||
toolchain go1.27.0
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0
|
||||
github.com/KarpelesLab/reflink v1.0.2
|
||||
github.com/KirCute/zip v1.0.1
|
||||
@@ -29,8 +29,8 @@ require (
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260701064823-d8c5600d73b8
|
||||
github.com/coreos/go-oidc v2.3.0+incompatible
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260803221621-0b399b9d036c
|
||||
github.com/coreos/go-oidc v2.5.0+incompatible
|
||||
github.com/deckarep/golang-set/v2 v2.9.0
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
@@ -75,9 +75,9 @@ require (
|
||||
github.com/u2takey/ffmpeg-go v0.5.0
|
||||
github.com/upyun/go-sdk/v3 v3.0.4
|
||||
github.com/zzzhr1990/go-common-entity v0.0.0-20250202070650-1a200048f0d3
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/crypto v0.55.0
|
||||
golang.org/x/image v0.44.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/appengine v1.6.8
|
||||
@@ -309,7 +309,7 @@ require (
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.40.0
|
||||
golang.org/x/text v0.41.0
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260715232425-e75dac1f907d // indirect
|
||||
google.golang.org/grpc v1.84.0-dev.0.20260723093437-b6eac429d7b6
|
||||
|
||||
@@ -13,6 +13,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEB
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1/go.mod h1:iptorS+VYKFL2N6PnebpS91dubG35eAOEERnT4PJbQU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||
@@ -300,12 +302,16 @@ github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64
|
||||
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260701064823-d8c5600d73b8 h1:+KC2I+emnT6ccWmo7IHbtchGc/vFqAxUuovo2eUgMuM=
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260701064823-d8c5600d73b8/go.mod h1:1pQXB0vAlzRlqcY7LYKOOZMw0wKfJPFxTLsJRF2Gswo=
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260803221621-0b399b9d036c h1:7VByb9X2X3LXbk89eMavoQO9uD5dcAjY6uPZhAR9Yso=
|
||||
github.com/cloudsoda/go-smb2 v0.0.0-20260803221621-0b399b9d036c/go.mod h1:1pQXB0vAlzRlqcY7LYKOOZMw0wKfJPFxTLsJRF2Gswo=
|
||||
github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc h1:0xCWmFKBmarCqqqLeM7jFBSw/Or81UEElFqO8MY+GDs=
|
||||
github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc/go.mod h1:uvR42Hb/t52HQd7x5/ZLzZEK8oihrFpgnodIJ1vte2E=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0=
|
||||
github.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-oidc v2.5.0+incompatible h1:6W0vGJR3Tu0r0PwfmjOrRZSlfxeEln8dsejt3ZWIvwo=
|
||||
github.com/coreos/go-oidc v2.5.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
@@ -920,6 +926,8 @@ golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
@@ -956,6 +964,8 @@ golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1020,6 +1030,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
|
||||
+11
-3
@@ -19,6 +19,10 @@ type ObjUnwrap interface {
|
||||
Unwrap() Obj
|
||||
}
|
||||
|
||||
type ObjDisplayName interface {
|
||||
GetDisplayName() string
|
||||
}
|
||||
|
||||
type Obj interface {
|
||||
GetSize() int64
|
||||
GetName() string
|
||||
@@ -132,13 +136,17 @@ func ExtractFolder(objs []Obj, extractFolder string) {
|
||||
})
|
||||
}
|
||||
|
||||
func WrapObjName(objs Obj) Obj {
|
||||
return &ObjWrapName{Name: utils.MappingName(objs.GetName()), Obj: objs}
|
||||
func WrapObjName(obj Obj) Obj {
|
||||
name := obj.GetName()
|
||||
if displayName, ok := obj.(ObjDisplayName); ok {
|
||||
name = displayName.GetDisplayName()
|
||||
}
|
||||
return &ObjWrapName{Name: utils.MappingName(name), Obj: obj}
|
||||
}
|
||||
|
||||
func WrapObjsName(objs []Obj) {
|
||||
for i := range objs {
|
||||
objs[i] = &ObjWrapName{Name: utils.MappingName(objs[i].GetName()), Obj: objs[i]}
|
||||
objs[i] = WrapObjName(objs[i])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user