Skip to content

Commit faec695

Browse files
csg-pr-botDev AgentZhang ZeHua
authored
Add upload and git import skills (#957)
* Add upload and git import skills * Fix lint * Fix test * Fix lint --------- Co-authored-by: Dev Agent <dev-agent@example.com> Co-authored-by: Zhang ZeHua <zh.zhang@opencsg.com>
1 parent 234f252 commit faec695

File tree

15 files changed

+1190
-49
lines changed

15 files changed

+1190
-49
lines changed

_mocks/opencsg.com/csghub-server/builder/store/s3/mock_Client.go

Lines changed: 68 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_SkillComponent.go

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

api/handler/skill.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/gin-gonic/gin"
1111
"opencsg.com/csghub-server/api/httpbase"
12+
"opencsg.com/csghub-server/builder/store/s3"
1213
"opencsg.com/csghub-server/common/config"
1314
"opencsg.com/csghub-server/common/errorx"
1415
"opencsg.com/csghub-server/common/types"
@@ -29,17 +30,25 @@ func NewSkillHandler(config *config.Config) (*SkillHandler, error) {
2930
if err != nil {
3031
return nil, fmt.Errorf("error creating repo component:%w", err)
3132
}
33+
s3Client, err := s3.NewMinio(config)
34+
if err != nil {
35+
return nil, fmt.Errorf("error creating s3 client:%w", err)
36+
}
3237
return &SkillHandler{
3338
skill: tc,
3439
sensitive: sc,
3540
repo: repo,
41+
s3Client: s3Client,
42+
config: config,
3643
}, nil
3744
}
3845

3946
type SkillHandler struct {
4047
skill component.SkillComponent
4148
sensitive component.SensitiveComponent
4249
repo component.RepoComponent
50+
s3Client s3.Client
51+
config *config.Config
4352
}
4453

4554
// CreateSkill godoc
@@ -338,3 +347,31 @@ func (h *SkillHandler) Relations(ctx *gin.Context) {
338347

339348
httpbase.OK(ctx, detail)
340349
}
350+
351+
// UploadSkillPackage godoc
352+
// @Security ApiKey
353+
// @Summary Get skill package upload URL
354+
// @Description Get a presigned URL and form data for uploading skill package
355+
// @Tags Skill
356+
// @Produce json
357+
// @Param current_user query string false "current user"
358+
// @Success 200 {object} types.Response{data=map[string]interface{}} "OK"
359+
// @Failure 400 {object} types.APIBadRequest "Bad request"
360+
// @Failure 500 {object} types.APIInternalServerError "Internal server error"
361+
// @Router /skills/upload_url [post]
362+
func (h *SkillHandler) GetUploadUrl(ctx *gin.Context) {
363+
// Call component to get upload URL, UUID, and form data
364+
url, uuid, formData, err := h.skill.GetUploadUrl(ctx.Request.Context())
365+
if err != nil {
366+
slog.ErrorContext(ctx.Request.Context(), "Failed to get upload URL", slog.Any("error", err))
367+
httpbase.ServerError(ctx, err)
368+
return
369+
}
370+
371+
// Return the upload URL, UUID, and form data
372+
httpbase.OK(ctx, map[string]interface{}{
373+
"url": url,
374+
"uuid": uuid,
375+
"formData": formData,
376+
})
377+
}

api/handler/skill_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package handler
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
"net/http"
7+
"net/http/httptest"
58
"testing"
69

710
"github.com/alibabacloud-go/tea/tea"
811
"github.com/gin-gonic/gin"
12+
"github.com/stretchr/testify/mock"
913
"github.com/stretchr/testify/require"
1014
mockcomponent "opencsg.com/csghub-server/_mocks/opencsg.com/csghub-server/component"
1115
"opencsg.com/csghub-server/builder/testutil"
16+
"opencsg.com/csghub-server/common/config"
1217
"opencsg.com/csghub-server/common/types"
1318
)
1419

@@ -169,3 +174,74 @@ func TestSkillHandler_Relations(t *testing.T) {
169174
tester.WithUser().Execute()
170175
tester.ResponseEq(t, 200, tester.OKText, &types.Relations{})
171176
}
177+
178+
func TestSkillHandler_GetUploadUrl(t *testing.T) {
179+
// Setup test configuration
180+
cfg := &config.Config{}
181+
cfg.S3.Bucket = "test-bucket"
182+
183+
// Create mock components
184+
mockSkillComponent := mockcomponent.NewMockSkillComponent(t)
185+
mockSensitiveComponent := mockcomponent.NewMockSensitiveComponent(t)
186+
mockRepoComponent := mockcomponent.NewMockRepoComponent(t)
187+
188+
// Expected upload URL, UUID, and form data
189+
expectedURL := "http://example.com/upload"
190+
expectedUUID := "test-uuid"
191+
expectedFormData := map[string]string{
192+
"key": "skills/packages/test-uuid",
193+
"policy": "test-policy",
194+
"x-amz-signature": "test-signature",
195+
}
196+
197+
// Set up mock expectations
198+
mockSkillComponent.EXPECT().GetUploadUrl(mock.Anything).Return(expectedURL, expectedUUID, expectedFormData, nil)
199+
200+
// Create skill handler with mock dependencies
201+
handler := &SkillHandler{
202+
skill: mockSkillComponent,
203+
sensitive: mockSensitiveComponent,
204+
repo: mockRepoComponent,
205+
config: cfg,
206+
}
207+
208+
// Create a test HTTP request
209+
req, err := http.NewRequest("POST", "/skills/upload_url", nil)
210+
require.Nil(t, err)
211+
// Set the current user header
212+
req.Header.Set("X-User", "test-user")
213+
214+
// Create a test HTTP response recorder
215+
w := httptest.NewRecorder()
216+
217+
// Create a gin context
218+
ctx, _ := gin.CreateTestContext(w)
219+
ctx.Request = req
220+
// Set current user in context
221+
ctx.Set("currentUser", "test-user")
222+
223+
// Call the GetUploadUrl method
224+
handler.GetUploadUrl(ctx)
225+
226+
// Check the response
227+
require.Equal(t, http.StatusOK, w.Code)
228+
229+
// Check the response body
230+
var response struct {
231+
Msg string `json:"msg"`
232+
Data map[string]interface{} `json:"data"`
233+
}
234+
err = json.Unmarshal(w.Body.Bytes(), &response)
235+
require.Nil(t, err)
236+
require.Equal(t, expectedURL, response.Data["url"])
237+
require.Equal(t, expectedUUID, response.Data["uuid"])
238+
239+
// Convert formData to map[string]string for comparison
240+
formData, ok := response.Data["formData"].(map[string]interface{})
241+
require.True(t, ok)
242+
expectedFormDataMap := make(map[string]interface{})
243+
for k, v := range expectedFormData {
244+
expectedFormDataMap[k] = v
245+
}
246+
require.Equal(t, expectedFormDataMap, formData)
247+
}

api/router/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,6 +1462,11 @@ func createSkillRoutes(
14621462
repoCommonHandler *handler.RepoHandler,
14631463
) {
14641464
skillGroup := apiGroup.Group("/skills")
1465+
1466+
// Upload skill package endpoint - doesn't require existing repo
1467+
skillGroup.POST("/upload_url", middlewareCollection.Auth.NeedPhoneVerified, skillHandler.GetUploadUrl)
1468+
1469+
// Routes that require existing repo
14651470
skillGroup.Use(middleware.RepoType(types.SkillRepo), middlewareCollection.Repo.RepoExists)
14661471
{
14671472
skillGroup.POST("", middlewareCollection.Auth.NeedPhoneVerified, skillHandler.Create)

builder/store/s3/minio.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type MinioClient interface {
7272
CopyObject(ctx context.Context, dst minio.CopyDestOptions, src minio.CopySrcOptions) (minio.UploadInfo, error)
7373
GetObject(ctx context.Context, bucketName, objectName string, opts minio.GetObjectOptions) (*minio.Object, error)
7474
ListObjects(ctx context.Context, bucketName string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
75+
PresignedPostPolicy(ctx context.Context, policy *minio.PostPolicy) (u *url.URL, formData map[string]string, err error)
7576
}
7677

7778
type Client interface {

common/types/skill.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import "time"
44

55
type CreateSkillReq struct {
66
CreateRepoReq
7+
// Skill package SHA256 hash
8+
SkillPackageSHA256 string `json:"skill_file"`
9+
// Git repository URL for mirroring
10+
GitURL string `json:"git_url"`
11+
// Git username for authentication
12+
GitUsername string `json:"git_username"`
13+
// Git password for authentication
14+
GitPassword string `json:"git_password"`
715
}
816

917
type UpdateSkillReq struct {

common/utils/common/repo.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,8 @@ func ShortenCommitID7(fullCommitID string) (string, error) {
256256
}
257257
return commitID[:7], nil
258258
}
259+
260+
// BuildSkillPackageObjectKey builds the object key for skill package in object storage
261+
func BuildSkillPackageObjectKey(sha256 string) string {
262+
return fmt.Sprintf("skills/packages/%s", sha256)
263+
}

common/utils/common/repo_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,17 @@ func TestBuildHashedRelativePath(t *testing.T) {
373373
res := BuildHashedRelativePath(1)
374374
require.Equal(t, "@hashed_repos/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b.git", res)
375375
}
376+
377+
func TestBuildSkillPackageObjectKey(t *testing.T) {
378+
// Test with a sample SHA256 hash
379+
sha256 := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
380+
expectedObjectKey := "skills/packages/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
381+
actualObjectKey := BuildSkillPackageObjectKey(sha256)
382+
require.Equal(t, expectedObjectKey, actualObjectKey)
383+
384+
// Test with another SHA256 hash
385+
sha256 = "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8"
386+
expectedObjectKey = "skills/packages/5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8"
387+
actualObjectKey = BuildSkillPackageObjectKey(sha256)
388+
require.Equal(t, expectedObjectKey, actualObjectKey)
389+
}

0 commit comments

Comments
 (0)