Skip to content

Commit b238137

Browse files
committed
Add batch get repository size api
1 parent a0ccabb commit b238137

15 files changed

Lines changed: 627 additions & 5 deletions

File tree

_mocks/opencsg.com/csghub-server/builder/store/database/mock_RepositoryStatisticsStore.go

Lines changed: 59 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

_mocks/opencsg.com/csghub-server/component/mock_RepoComponent.go

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/handler/repo.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,6 +2046,47 @@ func (h *RepoHandler) GetRepoSizeByBranch(ctx *gin.Context) {
20462046
httpbase.OK(ctx, size)
20472047
}
20482048

2049+
// BatchGetRepoExtra godoc
2050+
// @Security ApiKey
2051+
// @Summary Batch get extra information for multiple repositories
2052+
// @Tags Repository
2053+
// @Accept json
2054+
// @Produce json
2055+
// @Param current_user query string false "current user name"
2056+
// @Param body body types.BatchRepoExtraReq true "request body"
2057+
// @Success 200 {object} types.Response{data=[]types.RepoExtraItem} "OK"
2058+
// @Failure 400 {object} types.APIBadRequest "Bad request"
2059+
// @Failure 500 {object} types.APIInternalServerError "Internal server error"
2060+
// @Router /repos/extra [post]
2061+
func (h *RepoHandler) BatchGetRepoExtra(ctx *gin.Context) {
2062+
var req types.BatchRepoExtraReq
2063+
if err := ctx.ShouldBindJSON(&req); err != nil {
2064+
slog.ErrorContext(ctx.Request.Context(), "Bad request format", "error", err)
2065+
httpbase.BadRequestWithExt(ctx, errorx.ReqBodyFormat(err, nil))
2066+
return
2067+
}
2068+
2069+
if len(req.RepoIDs) > h.config.MaxRepoBatchNum {
2070+
httpbase.BadRequestWithExt(ctx, errorx.ReqParamInvalid(
2071+
fmt.Errorf("too many repository ids, max %d, got %d", h.config.MaxRepoBatchNum, len(req.RepoIDs)),
2072+
errorx.Ctx().Set("max", h.config.MaxRepoBatchNum).Set("got", len(req.RepoIDs)),
2073+
))
2074+
return
2075+
}
2076+
2077+
currentUser := httpbase.GetCurrentUser(ctx)
2078+
2079+
extras, err := h.c.BatchGetRepoExtra(ctx.Request.Context(), req.RepoIDs, currentUser)
2080+
if err != nil {
2081+
slog.ErrorContext(ctx.Request.Context(), "Failed to batch get repo extras", slog.Any("error", err))
2082+
httpbase.ServerError(ctx, err)
2083+
return
2084+
}
2085+
2086+
slog.Debug("Batch get repo extras succeed", slog.Int("count", len(extras)))
2087+
httpbase.OK(ctx, extras)
2088+
}
2089+
20492090
// DownloadCodeZip godoc
20502091
// @Summary Download code repository as zip archive
20512092
// @Description Download code repository as zip archive

api/handler/repo_test.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ func NewRepoTester(t *testing.T) *RepoTester {
5656
m: tester.mocks.model,
5757
d: tester.mocks.dataset,
5858
temporal: tester.mocks.workflow,
59-
config: &config.Config{},
59+
config: &config.Config{
60+
MaxRepoBatchNum: 500,
61+
},
6062
}
6163
tester.WithParam("name", "r")
6264
tester.WithParam("namespace", "u")
@@ -1975,3 +1977,60 @@ func TestRepoHandler_GetRepos(t *testing.T) {
19751977
t, 200, tester.OKText, []string{},
19761978
)
19771979
}
1980+
1981+
func TestRepoHandler_BatchGetRepoExtra(t *testing.T) {
1982+
t.Run("success", func(t *testing.T) {
1983+
tester := NewRepoTester(t).WithHandleFunc(func(rp *RepoHandler) gin.HandlerFunc {
1984+
return rp.BatchGetRepoExtra
1985+
})
1986+
tester.WithBody(t, types.BatchRepoExtraReq{RepoIDs: []int64{1, 2}}).WithUser()
1987+
1988+
tester.mocks.repo.EXPECT().BatchGetRepoExtra(tester.Ctx(), []int64{1, 2}, "u").Return([]types.RepoExtraItem{{RepoID: 1, Size: 100}, {RepoID: 2, Size: 200}}, nil)
1989+
1990+
tester.Execute()
1991+
1992+
tester.ResponseEq(t, http.StatusOK, tester.OKText, []types.RepoExtraItem{
1993+
{RepoID: 1, Size: 100},
1994+
{RepoID: 2, Size: 200},
1995+
})
1996+
})
1997+
1998+
t.Run("too many IDs", func(t *testing.T) {
1999+
ids := make([]int64, 501)
2000+
for i := range ids {
2001+
ids[i] = int64(i + 1)
2002+
}
2003+
tester := NewRepoTester(t).WithHandleFunc(func(rp *RepoHandler) gin.HandlerFunc {
2004+
return rp.BatchGetRepoExtra
2005+
})
2006+
tester.WithBody(t, types.BatchRepoExtraReq{RepoIDs: ids}).WithUser()
2007+
2008+
tester.Execute()
2009+
2010+
tester.ResponseEqCode(t, http.StatusBadRequest)
2011+
})
2012+
2013+
t.Run("invalid JSON body", func(t *testing.T) {
2014+
tester := NewRepoTester(t).WithHandleFunc(func(rp *RepoHandler) gin.HandlerFunc {
2015+
return rp.BatchGetRepoExtra
2016+
})
2017+
tester.WithBody(t, "not a valid json").WithUser()
2018+
2019+
tester.Execute()
2020+
2021+
tester.ResponseEqCode(t, http.StatusBadRequest)
2022+
})
2023+
2024+
t.Run("server error", func(t *testing.T) {
2025+
tester := NewRepoTester(t).WithHandleFunc(func(rp *RepoHandler) gin.HandlerFunc {
2026+
return rp.BatchGetRepoExtra
2027+
})
2028+
tester.WithBody(t, types.BatchRepoExtraReq{RepoIDs: []int64{1}}).WithUser()
2029+
2030+
tester.mocks.repo.EXPECT().BatchGetRepoExtra(tester.Ctx(), []int64{1}, "u").Return(nil, errors.New("db error"))
2031+
2032+
tester.Execute()
2033+
2034+
tester.ResponseEqCode(t, http.StatusInternalServerError)
2035+
})
2036+
}

api/router/api.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,9 @@ func NewRouter(config *config.Config, enableSwagger bool) (*gin.Engine, error) {
421421
// Admin user get repo path list
422422
adminGroup.GET("/repos", repoCommonHandler.GetRepos)
423423

424+
// Batch get repository extra information for multiple repositories
425+
apiGroup.POST("/repos/extra", repoCommonHandler.BatchGetRepoExtra)
426+
424427
// routes for broadcast
425428
broadcastHandler, err := handler.NewBroadcastHandler()
426429
if err != nil {

builder/store/database/repository_statistics.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"time"
66

7+
"github.com/uptrace/bun"
78
"opencsg.com/csghub-server/common/errorx"
89
)
910

@@ -32,6 +33,9 @@ type RepositoryStatisticsStore interface {
3233
// FindByRepositoryIDAndBranch finds repository statistics by repository ID and branch
3334
FindByRepositoryIDAndBranch(ctx context.Context, repoID int64, branch string) (*RepositoryStatistics, error)
3435

36+
// FindByRepositoryIDs finds repository statistics for multiple repository IDs
37+
FindByRepositoryIDs(ctx context.Context, repoIDs []int64) ([]*RepositoryStatistics, error)
38+
3539
// Update updates repository statistics
3640
Update(ctx context.Context, stats *RepositoryStatistics) error
3741

@@ -87,6 +91,19 @@ func (s *RepositoryStatisticsStoreImpl) FindByRepositoryIDAndBranch(ctx context.
8791
return &stats, nil
8892
}
8993

94+
// FindByRepositoryIDs finds repository statistics for multiple repository IDs
95+
func (s *RepositoryStatisticsStoreImpl) FindByRepositoryIDs(ctx context.Context, repoIDs []int64) ([]*RepositoryStatistics, error) {
96+
if len(repoIDs) == 0 {
97+
return nil, nil
98+
}
99+
var stats []*RepositoryStatistics
100+
_, err := s.db.Operator.Core.NewSelect().Model(&stats).Where("repository_id IN (?)", bun.In(repoIDs)).Exec(ctx, &stats)
101+
if err != nil {
102+
return nil, errorx.HandleDBError(err, nil)
103+
}
104+
return stats, nil
105+
}
106+
90107
// Update updates repository statistics
91108
func (s *RepositoryStatisticsStoreImpl) Update(ctx context.Context, stats *RepositoryStatistics) error {
92109
_, err := s.db.Operator.Core.NewUpdate().Model(stats).Where("id = ?", stats.ID).Exec(ctx)

builder/store/database/repository_statistics_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,78 @@ func TestRepositoryStatisticsStore_FindByRepositoryIDAndBranch(t *testing.T) {
240240
_, err = store.FindByRepositoryIDAndBranch(ctx, expectedStats.RepositoryID, "non-existent-branch")
241241
assert.Error(t, err)
242242
}
243+
244+
func TestRepositoryStatisticsStore_FindByRepositoryIDs(t *testing.T) {
245+
ctx := context.Background()
246+
db := tests.InitTestDB()
247+
defer db.Close()
248+
249+
store := database.NewRepositoryStatisticsStoreWithDB(db)
250+
251+
// Create test statistics for multiple repositories
252+
stats1 := &database.RepositoryStatistics{
253+
RepositoryID: 8,
254+
Branch: "main",
255+
TotalSize: 1024,
256+
NonLfsSize: 512,
257+
LfsSize: 512,
258+
CreatedAt: time.Now(),
259+
UpdatedAt: time.Now(),
260+
}
261+
stats2 := &database.RepositoryStatistics{
262+
RepositoryID: 9,
263+
Branch: "main",
264+
TotalSize: 2048,
265+
NonLfsSize: 1024,
266+
LfsSize: 1024,
267+
CreatedAt: time.Now(),
268+
UpdatedAt: time.Now(),
269+
}
270+
stats3 := &database.RepositoryStatistics{
271+
RepositoryID: 10,
272+
Branch: "dev",
273+
TotalSize: 512,
274+
NonLfsSize: 256,
275+
LfsSize: 256,
276+
CreatedAt: time.Now(),
277+
UpdatedAt: time.Now(),
278+
}
279+
280+
err := store.Create(ctx, stats1)
281+
assert.NoError(t, err)
282+
err = store.Create(ctx, stats2)
283+
assert.NoError(t, err)
284+
err = store.Create(ctx, stats3)
285+
assert.NoError(t, err)
286+
287+
t.Run("find by multiple existing IDs", func(t *testing.T) {
288+
result, err := store.FindByRepositoryIDs(ctx, []int64{8, 9})
289+
assert.NoError(t, err)
290+
assert.Len(t, result, 2)
291+
})
292+
293+
t.Run("find by single ID", func(t *testing.T) {
294+
result, err := store.FindByRepositoryIDs(ctx, []int64{8})
295+
assert.NoError(t, err)
296+
assert.Len(t, result, 1)
297+
assert.Equal(t, int64(1024), result[0].TotalSize)
298+
})
299+
300+
t.Run("find by non-existent ID returns empty", func(t *testing.T) {
301+
result, err := store.FindByRepositoryIDs(ctx, []int64{999})
302+
assert.NoError(t, err)
303+
assert.Len(t, result, 0)
304+
})
305+
306+
t.Run("find by empty IDs returns nil", func(t *testing.T) {
307+
result, err := store.FindByRepositoryIDs(ctx, []int64{})
308+
assert.NoError(t, err)
309+
assert.Nil(t, result)
310+
})
311+
312+
t.Run("find by mixed existing and non-existing IDs", func(t *testing.T) {
313+
result, err := store.FindByRepositoryIDs(ctx, []int64{8, 999, 10})
314+
assert.NoError(t, err)
315+
assert.Len(t, result, 2)
316+
})
317+
}

common/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,8 @@ type Config struct {
728728
ActivityLog struct {
729729
Enabled bool `env:"STARHUB_SERVER_ACTIVITY_LOG_ENABLE" default:"true"`
730730
}
731+
732+
MaxRepoBatchNum int `env:"STARHUB_SERVER_MAX_REPO_SIZE_BATCH_NUM" default:"500"`
731733
}
732734

733735
type MemoryConfig struct {

0 commit comments

Comments
 (0)