diff --git a/internal/database/database.go b/internal/database/database.go index f1f74e67e..422800b6a 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -124,7 +124,6 @@ func NewConnection(w logger.Writer) (*gorm.DB, error) { // Initialize stores, sorted in alphabetical order. LoginSources = &loginSourcesStore{DB: db, files: sourceFiles} - LFS = &lfsStore{DB: db} Notices = NewNoticesStore(db) Orgs = NewOrgsStore(db) Perms = NewPermsStore(db) @@ -163,3 +162,7 @@ func (db *DB) AccessTokens() *AccessTokensStore { func (db *DB) Actions() *ActionsStore { return newActionsStore(db.db) } + +func (db *DB) LFS() *LFSStore { + return newLFSStore(db.db) +} diff --git a/internal/database/lfs.go b/internal/database/lfs.go index cf5616fd6..01717000f 100644 --- a/internal/database/lfs.go +++ b/internal/database/lfs.go @@ -6,6 +6,7 @@ package database import ( "context" + "errors" "fmt" "time" @@ -15,20 +16,6 @@ import ( "gogs.io/gogs/internal/lfsutil" ) -// LFSStore is the persistent interface for LFS objects. -type LFSStore interface { - // CreateObject creates a LFS object record in database. - CreateObject(ctx context.Context, repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error - // GetObjectByOID returns the LFS object with given OID. It returns - // ErrLFSObjectNotExist when not found. - GetObjectByOID(ctx context.Context, repoID int64, oid lfsutil.OID) (*LFSObject, error) - // GetObjectsByOIDs returns LFS objects found within "oids". The returned list - // could have less elements if some oids were not found. - GetObjectsByOIDs(ctx context.Context, repoID int64, oids ...lfsutil.OID) ([]*LFSObject, error) -} - -var LFS LFSStore - // LFSObject is the relation between an LFS object and a repository. type LFSObject struct { RepoID int64 `gorm:"primaryKey;auto_increment:false"` @@ -38,20 +25,24 @@ type LFSObject struct { CreatedAt time.Time `gorm:"not null"` } -var _ LFSStore = (*lfsStore)(nil) - -type lfsStore struct { - *gorm.DB +// LFSStore is the storage layer for LFS objects. +type LFSStore struct { + db *gorm.DB } -func (s *lfsStore) CreateObject(ctx context.Context, repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error { +func newLFSStore(db *gorm.DB) *LFSStore { + return &LFSStore{db: db} +} + +// CreateObject creates an LFS object record in database. +func (s *LFSStore) CreateObject(ctx context.Context, repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error { object := &LFSObject{ RepoID: repoID, OID: oid, Size: size, Storage: storage, } - return s.WithContext(ctx).Create(object).Error + return s.db.WithContext(ctx).Create(object).Error } type ErrLFSObjectNotExist struct { @@ -59,8 +50,7 @@ type ErrLFSObjectNotExist struct { } func IsErrLFSObjectNotExist(err error) bool { - _, ok := err.(ErrLFSObjectNotExist) - return ok + return errors.As(err, &ErrLFSObjectNotExist{}) } func (err ErrLFSObjectNotExist) Error() string { @@ -71,11 +61,13 @@ func (ErrLFSObjectNotExist) NotFound() bool { return true } -func (s *lfsStore) GetObjectByOID(ctx context.Context, repoID int64, oid lfsutil.OID) (*LFSObject, error) { +// GetObjectByOID returns the LFS object with given OID. It returns +// ErrLFSObjectNotExist when not found. +func (s *LFSStore) GetObjectByOID(ctx context.Context, repoID int64, oid lfsutil.OID) (*LFSObject, error) { object := new(LFSObject) - err := s.WithContext(ctx).Where("repo_id = ? AND oid = ?", repoID, oid).First(object).Error + err := s.db.WithContext(ctx).Where("repo_id = ? AND oid = ?", repoID, oid).First(object).Error if err != nil { - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrLFSObjectNotExist{args: errutil.Args{"repoID": repoID, "oid": oid}} } return nil, err @@ -83,14 +75,16 @@ func (s *lfsStore) GetObjectByOID(ctx context.Context, repoID int64, oid lfsutil return object, err } -func (s *lfsStore) GetObjectsByOIDs(ctx context.Context, repoID int64, oids ...lfsutil.OID) ([]*LFSObject, error) { +// GetObjectsByOIDs returns LFS objects found within "oids". The returned list +// could have fewer elements if some oids were not found. +func (s *LFSStore) GetObjectsByOIDs(ctx context.Context, repoID int64, oids ...lfsutil.OID) ([]*LFSObject, error) { if len(oids) == 0 { return []*LFSObject{}, nil } objects := make([]*LFSObject, 0, len(oids)) - err := s.WithContext(ctx).Where("repo_id = ? AND oid IN (?)", repoID, oids).Find(&objects).Error - if err != nil && err != gorm.ErrRecordNotFound { + err := s.db.WithContext(ctx).Where("repo_id = ? AND oid IN (?)", repoID, oids).Find(&objects).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } return objects, nil diff --git a/internal/database/lfs_test.go b/internal/database/lfs_test.go index cfff2fe31..6f6ef9a0e 100644 --- a/internal/database/lfs_test.go +++ b/internal/database/lfs_test.go @@ -23,13 +23,13 @@ func TestLFS(t *testing.T) { t.Parallel() ctx := context.Background() - db := &lfsStore{ - DB: newTestDB(t, "lfsStore"), + s := &LFSStore{ + db: newTestDB(t, "LFSStore"), } for _, tc := range []struct { name string - test func(t *testing.T, ctx context.Context, db *lfsStore) + test func(t *testing.T, ctx context.Context, s *LFSStore) }{ {"CreateObject", lfsCreateObject}, {"GetObjectByOID", lfsGetObjectByOID}, @@ -37,10 +37,10 @@ func TestLFS(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { - err := clearTables(t, db.DB) + err := clearTables(t, s.db) require.NoError(t, err) }) - tc.test(t, ctx, db) + tc.test(t, ctx, s) }) if t.Failed() { break @@ -48,52 +48,52 @@ func TestLFS(t *testing.T) { } } -func lfsCreateObject(t *testing.T, ctx context.Context, db *lfsStore) { +func lfsCreateObject(t *testing.T, ctx context.Context, s *LFSStore) { // Create first LFS object repoID := int64(1) oid := lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f") - err := db.CreateObject(ctx, repoID, oid, 12, lfsutil.StorageLocal) + err := s.CreateObject(ctx, repoID, oid, 12, lfsutil.StorageLocal) require.NoError(t, err) // Get it back and check the CreatedAt field - object, err := db.GetObjectByOID(ctx, repoID, oid) + object, err := s.GetObjectByOID(ctx, repoID, oid) require.NoError(t, err) - assert.Equal(t, db.NowFunc().Format(time.RFC3339), object.CreatedAt.UTC().Format(time.RFC3339)) + assert.Equal(t, s.db.NowFunc().Format(time.RFC3339), object.CreatedAt.UTC().Format(time.RFC3339)) - // Try create second LFS object with same oid should fail - err = db.CreateObject(ctx, repoID, oid, 12, lfsutil.StorageLocal) + // Try to create second LFS object with same oid should fail + err = s.CreateObject(ctx, repoID, oid, 12, lfsutil.StorageLocal) assert.Error(t, err) } -func lfsGetObjectByOID(t *testing.T, ctx context.Context, db *lfsStore) { +func lfsGetObjectByOID(t *testing.T, ctx context.Context, s *LFSStore) { // Create a LFS object repoID := int64(1) oid := lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f") - err := db.CreateObject(ctx, repoID, oid, 12, lfsutil.StorageLocal) + err := s.CreateObject(ctx, repoID, oid, 12, lfsutil.StorageLocal) require.NoError(t, err) // We should be able to get it back - _, err = db.GetObjectByOID(ctx, repoID, oid) + _, err = s.GetObjectByOID(ctx, repoID, oid) require.NoError(t, err) // Try to get a non-existent object - _, err = db.GetObjectByOID(ctx, repoID, "bad_oid") + _, err = s.GetObjectByOID(ctx, repoID, "bad_oid") expErr := ErrLFSObjectNotExist{args: errutil.Args{"repoID": repoID, "oid": lfsutil.OID("bad_oid")}} assert.Equal(t, expErr, err) } -func lfsGetObjectsByOIDs(t *testing.T, ctx context.Context, db *lfsStore) { +func lfsGetObjectsByOIDs(t *testing.T, ctx context.Context, s *LFSStore) { // Create two LFS objects repoID := int64(1) oid1 := lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f") oid2 := lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64g") - err := db.CreateObject(ctx, repoID, oid1, 12, lfsutil.StorageLocal) + err := s.CreateObject(ctx, repoID, oid1, 12, lfsutil.StorageLocal) require.NoError(t, err) - err = db.CreateObject(ctx, repoID, oid2, 12, lfsutil.StorageLocal) + err = s.CreateObject(ctx, repoID, oid2, 12, lfsutil.StorageLocal) require.NoError(t, err) // We should be able to get them back and ignore non-existent ones - objects, err := db.GetObjectsByOIDs(ctx, repoID, oid1, oid2, "bad_oid") + objects, err := s.GetObjectsByOIDs(ctx, repoID, oid1, oid2, "bad_oid") require.NoError(t, err) assert.Equal(t, 2, len(objects), "number of objects") diff --git a/internal/database/mocks.go b/internal/database/mocks.go index f412f5a9f..6119787d4 100644 --- a/internal/database/mocks.go +++ b/internal/database/mocks.go @@ -8,14 +8,6 @@ import ( "testing" ) -func SetMockLFSStore(t *testing.T, mock LFSStore) { - before := LFS - LFS = mock - t.Cleanup(func() { - LFS = before - }) -} - func setMockLoginSourcesStore(t *testing.T, mock LoginSourcesStore) { before := LoginSources LoginSources = mock diff --git a/internal/route/lfs/basic.go b/internal/route/lfs/basic.go index 4c7817265..1bcaf789a 100644 --- a/internal/route/lfs/basic.go +++ b/internal/route/lfs/basic.go @@ -25,6 +25,7 @@ const ( ) type basicHandler struct { + store Store // The default storage backend for uploading new objects. defaultStorage lfsutil.Storage // The list of available storage backends to access objects. @@ -43,7 +44,7 @@ func (h *basicHandler) Storager(storage lfsutil.Storage) lfsutil.Storager { // GET /{owner}/{repo}.git/info/lfs/object/basic/{oid} func (h *basicHandler) serveDownload(c *macaron.Context, repo *database.Repository, oid lfsutil.OID) { - object, err := database.LFS.GetObjectByOID(c.Req.Context(), repo.ID, oid) + object, err := h.store.GetLFSObjectByOID(c.Req.Context(), repo.ID, oid) if err != nil { if database.IsErrLFSObjectNotExist(err) { responseJSON(c.Resp, http.StatusNotFound, responseError{ @@ -78,7 +79,7 @@ func (h *basicHandler) serveDownload(c *macaron.Context, repo *database.Reposito func (h *basicHandler) serveUpload(c *macaron.Context, repo *database.Repository, oid lfsutil.OID) { // NOTE: LFS client will retry upload the same object if there was a partial failure, // therefore we would like to skip ones that already exist. - _, err := database.LFS.GetObjectByOID(c.Req.Context(), repo.ID, oid) + _, err := h.store.GetLFSObjectByOID(c.Req.Context(), repo.ID, oid) if err == nil { // Object exists, drain the request body and we're good. _, _ = io.Copy(io.Discard, c.Req.Request.Body) @@ -105,7 +106,7 @@ func (h *basicHandler) serveUpload(c *macaron.Context, repo *database.Repository return } - err = database.LFS.CreateObject(c.Req.Context(), repo.ID, oid, written, s.Storage()) + err = h.store.CreateLFSObject(c.Req.Context(), repo.ID, oid, written, s.Storage()) if err != nil { // NOTE: It is OK to leave the file when the whole operation failed // with a DB error, a retry on client side can safely overwrite the @@ -120,7 +121,7 @@ func (h *basicHandler) serveUpload(c *macaron.Context, repo *database.Repository } // POST /{owner}/{repo}.git/info/lfs/object/basic/verify -func (*basicHandler) serveVerify(c *macaron.Context, repo *database.Repository) { +func (h *basicHandler) serveVerify(c *macaron.Context, repo *database.Repository) { var request basicVerifyRequest defer func() { _ = c.Req.Request.Body.Close() }() @@ -139,7 +140,7 @@ func (*basicHandler) serveVerify(c *macaron.Context, repo *database.Repository) return } - object, err := database.LFS.GetObjectByOID(c.Req.Context(), repo.ID, request.Oid) + object, err := h.store.GetLFSObjectByOID(c.Req.Context(), repo.ID, request.Oid) if err != nil { if database.IsErrLFSObjectNotExist(err) { responseJSON(c.Resp, http.StatusNotFound, responseError{ diff --git a/internal/route/lfs/basic_test.go b/internal/route/lfs/basic_test.go index 860e6daf5..7b2d75216 100644 --- a/internal/route/lfs/basic_test.go +++ b/internal/route/lfs/basic_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/macaron.v1" "gogs.io/gogs/internal/database" @@ -21,7 +22,7 @@ import ( var _ lfsutil.Storager = (*mockStorage)(nil) -// mockStorage is a in-memory storage for LFS objects. +// mockStorage is an in-memory storage for LFS objects. type mockStorage struct { buf *bytes.Buffer } @@ -31,7 +32,7 @@ func (*mockStorage) Storage() lfsutil.Storage { } func (s *mockStorage) Upload(_ lfsutil.OID, rc io.ReadCloser) (int64, error) { - defer rc.Close() + defer func() { _ = rc.Close() }() return io.Copy(s.buf, rc) } @@ -40,7 +41,7 @@ func (s *mockStorage) Download(_ lfsutil.OID, w io.Writer) error { return err } -func Test_basicHandler_serveDownload(t *testing.T) { +func TestBasicHandler_serveDownload(t *testing.T) { s := &mockStorage{} basic := &basicHandler{ defaultStorage: s.Storage(), @@ -60,17 +61,17 @@ func Test_basicHandler_serveDownload(t *testing.T) { tests := []struct { name string content string - mockLFSStore func() database.LFSStore + mockStore func() *MockStore expStatusCode int expHeader http.Header expBody string }{ { name: "object does not exist", - mockLFSStore: func() database.LFSStore { - mock := NewMockLFSStore() - mock.GetObjectByOIDFunc.SetDefaultReturn(nil, database.ErrLFSObjectNotExist{}) - return mock + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(nil, database.ErrLFSObjectNotExist{}) + return mockStore }, expStatusCode: http.StatusNotFound, expHeader: http.Header{ @@ -80,10 +81,10 @@ func Test_basicHandler_serveDownload(t *testing.T) { }, { name: "storage not found", - mockLFSStore: func() database.LFSStore { - mock := NewMockLFSStore() - mock.GetObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{Storage: "bad_storage"}, nil) - return mock + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{Storage: "bad_storage"}, nil) + return mockStore }, expStatusCode: http.StatusInternalServerError, expHeader: http.Header{ @@ -95,16 +96,16 @@ func Test_basicHandler_serveDownload(t *testing.T) { { name: "object exists", content: "Hello world!", - mockLFSStore: func() database.LFSStore { - mock := NewMockLFSStore() - mock.GetObjectByOIDFunc.SetDefaultReturn( + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn( &database.LFSObject{ Size: 12, Storage: s.Storage(), }, nil, ) - return mock + return mockStore }, expStatusCode: http.StatusOK, expHeader: http.Header{ @@ -116,14 +117,12 @@ func Test_basicHandler_serveDownload(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - database.SetMockLFSStore(t, test.mockLFSStore()) + basic.store = test.mockStore() s.buf = bytes.NewBufferString(test.content) - r, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) - } + r, err := http.NewRequest(http.MethodGet, "/", nil) + require.NoError(t, err) rr := httptest.NewRecorder() m.ServeHTTP(rr, r) @@ -133,15 +132,13 @@ func Test_basicHandler_serveDownload(t *testing.T) { assert.Equal(t, test.expHeader, resp.Header) body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assert.Equal(t, test.expBody, string(body)) }) } } -func Test_basicHandler_serveUpload(t *testing.T) { +func TestBasicHandler_serveUpload(t *testing.T) { s := &mockStorage{buf: &bytes.Buffer{}} basic := &basicHandler{ defaultStorage: s.Storage(), @@ -160,37 +157,35 @@ func Test_basicHandler_serveUpload(t *testing.T) { tests := []struct { name string - mockLFSStore func() database.LFSStore + mockStore func() *MockStore expStatusCode int expBody string }{ { name: "object already exists", - mockLFSStore: func() database.LFSStore { - mock := NewMockLFSStore() - mock.GetObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{}, nil) - return mock + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{}, nil) + return mockStore }, expStatusCode: http.StatusOK, }, { name: "new object", - mockLFSStore: func() database.LFSStore { - mock := NewMockLFSStore() - mock.GetObjectByOIDFunc.SetDefaultReturn(nil, database.ErrLFSObjectNotExist{}) - return mock + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(nil, database.ErrLFSObjectNotExist{}) + return mockStore }, expStatusCode: http.StatusOK, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - database.SetMockLFSStore(t, test.mockLFSStore()) + basic.store = test.mockStore() r, err := http.NewRequest("PUT", "/", strings.NewReader("Hello world!")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) rr := httptest.NewRecorder() m.ServeHTTP(rr, r) @@ -199,26 +194,26 @@ func Test_basicHandler_serveUpload(t *testing.T) { assert.Equal(t, test.expStatusCode, resp.StatusCode) body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assert.Equal(t, test.expBody, string(body)) }) } } -func Test_basicHandler_serveVerify(t *testing.T) { +func TestBasicHandler_serveVerify(t *testing.T) { + basic := &basicHandler{} + m := macaron.New() m.Use(macaron.Renderer()) m.Use(func(c *macaron.Context) { c.Map(&database.Repository{Name: "repo"}) }) - m.Post("/", (&basicHandler{}).serveVerify) + m.Post("/", basic.serveVerify) tests := []struct { name string body string - mockLFSStore func() database.LFSStore + mockStore func() *MockStore expStatusCode int expBody string }{ @@ -231,10 +226,10 @@ func Test_basicHandler_serveVerify(t *testing.T) { { name: "object does not exist", body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"}`, - mockLFSStore: func() database.LFSStore { - mock := NewMockLFSStore() - mock.GetObjectByOIDFunc.SetDefaultReturn(nil, database.ErrLFSObjectNotExist{}) - return mock + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(nil, database.ErrLFSObjectNotExist{}) + return mockStore }, expStatusCode: http.StatusNotFound, expBody: `{"message":"Object does not exist"}` + "\n", @@ -242,10 +237,10 @@ func Test_basicHandler_serveVerify(t *testing.T) { { name: "object size mismatch", body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"}`, - mockLFSStore: func() database.LFSStore { - mock := NewMockLFSStore() - mock.GetObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{Size: 12}, nil) - return mock + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{Size: 12}, nil) + return mockStore }, expStatusCode: http.StatusBadRequest, expBody: `{"message":"Object size mismatch"}` + "\n", @@ -254,24 +249,22 @@ func Test_basicHandler_serveVerify(t *testing.T) { { name: "object exists", body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f", "size":12}`, - mockLFSStore: func() database.LFSStore { - mock := NewMockLFSStore() - mock.GetObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{Size: 12}, nil) - return mock + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetLFSObjectByOIDFunc.SetDefaultReturn(&database.LFSObject{Size: 12}, nil) + return mockStore }, expStatusCode: http.StatusOK, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - if test.mockLFSStore != nil { - database.SetMockLFSStore(t, test.mockLFSStore()) + if test.mockStore != nil { + basic.store = test.mockStore() } r, err := http.NewRequest("POST", "/", strings.NewReader(test.body)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) rr := httptest.NewRecorder() m.ServeHTTP(rr, r) @@ -280,9 +273,7 @@ func Test_basicHandler_serveVerify(t *testing.T) { assert.Equal(t, test.expStatusCode, resp.StatusCode) body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assert.Equal(t, test.expBody, string(body)) }) } diff --git a/internal/route/lfs/batch.go b/internal/route/lfs/batch.go index 8b20223a4..27a159f8d 100644 --- a/internal/route/lfs/batch.go +++ b/internal/route/lfs/batch.go @@ -19,111 +19,113 @@ import ( ) // POST /{owner}/{repo}.git/info/lfs/object/batch -func serveBatch(c *macaron.Context, owner *database.User, repo *database.Repository) { - var request batchRequest - defer c.Req.Request.Body.Close() - err := jsoniter.NewDecoder(c.Req.Request.Body).Decode(&request) - if err != nil { - responseJSON(c.Resp, http.StatusBadRequest, responseError{ - Message: strutil.ToUpperFirst(err.Error()), - }) - return - } - - // NOTE: We only support basic transfer as of now. - transfer := transferBasic - // Example: https://try.gogs.io/gogs/gogs.git/info/lfs/object/basic - baseHref := fmt.Sprintf("%s%s/%s.git/info/lfs/objects/basic", conf.Server.ExternalURL, owner.Name, repo.Name) - - objects := make([]batchObject, 0, len(request.Objects)) - switch request.Operation { - case basicOperationUpload: - for _, obj := range request.Objects { - var actions batchActions - if lfsutil.ValidOID(obj.Oid) { - actions = batchActions{ - Upload: &batchAction{ - Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid), - Header: map[string]string{ - // NOTE: git-lfs v2.5.0 sets the Content-Type based on the uploaded file. - // This ensures that the client always uses the designated value for the header. - "Content-Type": "application/octet-stream", - }, - }, - Verify: &batchAction{ - Href: fmt.Sprintf("%s/verify", baseHref), - }, - } - } else { - actions = batchActions{ - Error: &batchError{ - Code: http.StatusUnprocessableEntity, - Message: "Object has invalid oid", - }, - } - } - - objects = append(objects, batchObject{ - Oid: obj.Oid, - Size: obj.Size, - Actions: actions, - }) - } - - case basicOperationDownload: - oids := make([]lfsutil.OID, 0, len(request.Objects)) - for _, obj := range request.Objects { - oids = append(oids, obj.Oid) - } - stored, err := database.LFS.GetObjectsByOIDs(c.Req.Context(), repo.ID, oids...) +func serveBatch(store Store) macaron.Handler { + return func(c *macaron.Context, owner *database.User, repo *database.Repository) { + var request batchRequest + defer func() { _ = c.Req.Request.Body.Close() }() + err := jsoniter.NewDecoder(c.Req.Request.Body).Decode(&request) if err != nil { - internalServerError(c.Resp) - log.Error("Failed to get objects [repo_id: %d, oids: %v]: %v", repo.ID, oids, err) + responseJSON(c.Resp, http.StatusBadRequest, responseError{ + Message: strutil.ToUpperFirst(err.Error()), + }) return } - storedSet := make(map[lfsutil.OID]*database.LFSObject, len(stored)) - for _, obj := range stored { - storedSet[obj.OID] = obj - } - for _, obj := range request.Objects { - var actions batchActions - if stored := storedSet[obj.Oid]; stored != nil { - if stored.Size != obj.Size { - actions.Error = &batchError{ - Code: http.StatusUnprocessableEntity, - Message: "Object size mismatch", + // NOTE: We only support basic transfer as of now. + transfer := transferBasic + // Example: https://try.gogs.io/gogs/gogs.git/info/lfs/object/basic + baseHref := fmt.Sprintf("%s%s/%s.git/info/lfs/objects/basic", conf.Server.ExternalURL, owner.Name, repo.Name) + + objects := make([]batchObject, 0, len(request.Objects)) + switch request.Operation { + case basicOperationUpload: + for _, obj := range request.Objects { + var actions batchActions + if lfsutil.ValidOID(obj.Oid) { + actions = batchActions{ + Upload: &batchAction{ + Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid), + Header: map[string]string{ + // NOTE: git-lfs v2.5.0 sets the Content-Type based on the uploaded file. + // This ensures that the client always uses the designated value for the header. + "Content-Type": "application/octet-stream", + }, + }, + Verify: &batchAction{ + Href: fmt.Sprintf("%s/verify", baseHref), + }, } } else { - actions.Download = &batchAction{ - Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid), + actions = batchActions{ + Error: &batchError{ + Code: http.StatusUnprocessableEntity, + Message: "Object has invalid oid", + }, } } - } else { - actions.Error = &batchError{ - Code: http.StatusNotFound, - Message: "Object does not exist", - } + + objects = append(objects, batchObject{ + Oid: obj.Oid, + Size: obj.Size, + Actions: actions, + }) } - objects = append(objects, batchObject{ - Oid: obj.Oid, - Size: obj.Size, - Actions: actions, + case basicOperationDownload: + oids := make([]lfsutil.OID, 0, len(request.Objects)) + for _, obj := range request.Objects { + oids = append(oids, obj.Oid) + } + stored, err := store.GetLFSObjectsByOIDs(c.Req.Context(), repo.ID, oids...) + if err != nil { + internalServerError(c.Resp) + log.Error("Failed to get objects [repo_id: %d, oids: %v]: %v", repo.ID, oids, err) + return + } + storedSet := make(map[lfsutil.OID]*database.LFSObject, len(stored)) + for _, obj := range stored { + storedSet[obj.OID] = obj + } + + for _, obj := range request.Objects { + var actions batchActions + if stored := storedSet[obj.Oid]; stored != nil { + if stored.Size != obj.Size { + actions.Error = &batchError{ + Code: http.StatusUnprocessableEntity, + Message: "Object size mismatch", + } + } else { + actions.Download = &batchAction{ + Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid), + } + } + } else { + actions.Error = &batchError{ + Code: http.StatusNotFound, + Message: "Object does not exist", + } + } + + objects = append(objects, batchObject{ + Oid: obj.Oid, + Size: obj.Size, + Actions: actions, + }) + } + + default: + responseJSON(c.Resp, http.StatusBadRequest, responseError{ + Message: "Operation not recognized", }) + return } - default: - responseJSON(c.Resp, http.StatusBadRequest, responseError{ - Message: "Operation not recognized", + responseJSON(c.Resp, http.StatusOK, batchResponse{ + Transfer: transfer, + Objects: objects, }) - return } - - responseJSON(c.Resp, http.StatusOK, batchResponse{ - Transfer: transfer, - Objects: objects, - }) } // batchRequest defines the request payload for the batch endpoint. diff --git a/internal/route/lfs/batch_test.go b/internal/route/lfs/batch_test.go index c9e49e850..f6aa7a0fc 100644 --- a/internal/route/lfs/batch_test.go +++ b/internal/route/lfs/batch_test.go @@ -13,28 +13,22 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/macaron.v1" "gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/database" ) -func Test_serveBatch(t *testing.T) { +func TestServeBatch(t *testing.T) { conf.SetMockServer(t, conf.ServerOpts{ ExternalURL: "https://gogs.example.com/", }) - m := macaron.New() - m.Use(func(c *macaron.Context) { - c.Map(&database.User{Name: "owner"}) - c.Map(&database.Repository{Name: "repo"}) - }) - m.Post("/", serveBatch) - tests := []struct { name string body string - mockLFSStore func() database.LFSStore + mockStore func() *MockStore expStatusCode int expBody string }{ @@ -82,9 +76,9 @@ func Test_serveBatch(t *testing.T) { {"oid": "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f", "size": 123}, {"oid": "5cac0a318669fadfee734fb340a5f5b70b428ac57a9f4b109cb6e150b2ba7e57", "size": 456} ]}`, - mockLFSStore: func() database.LFSStore { - mock := NewMockLFSStore() - mock.GetObjectsByOIDsFunc.SetDefaultReturn( + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetLFSObjectsByOIDsFunc.SetDefaultReturn( []*database.LFSObject{ { OID: "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f", @@ -96,7 +90,7 @@ func Test_serveBatch(t *testing.T) { }, nil, ) - return mock + return mockStore }, expStatusCode: http.StatusOK, expBody: `{ @@ -123,14 +117,20 @@ func Test_serveBatch(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - if test.mockLFSStore != nil { - database.SetMockLFSStore(t, test.mockLFSStore()) + mockStore := NewMockStore() + if test.mockStore != nil { + mockStore = test.mockStore() } - r, err := http.NewRequest("POST", "/", bytes.NewBufferString(test.body)) - if err != nil { - t.Fatal(err) - } + m := macaron.New() + m.Use(func(c *macaron.Context) { + c.Map(&database.User{Name: "owner"}) + c.Map(&database.Repository{Name: "repo"}) + }) + m.Post("/", serveBatch(mockStore)) + + r, err := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(test.body)) + require.NoError(t, err) rr := httptest.NewRecorder() m.ServeHTTP(rr, r) @@ -139,21 +139,15 @@ func Test_serveBatch(t *testing.T) { assert.Equal(t, test.expStatusCode, resp.StatusCode) body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var expBody bytes.Buffer err = json.Indent(&expBody, []byte(test.expBody), "", " ") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var gotBody bytes.Buffer err = json.Indent(&gotBody, body, "", " ") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assert.Equal(t, expBody.String(), gotBody.String()) }) diff --git a/internal/route/lfs/mocks_test.go b/internal/route/lfs/mocks_test.go index c4fbb1f09..acf65dc5f 100644 --- a/internal/route/lfs/mocks_test.go +++ b/internal/route/lfs/mocks_test.go @@ -14,423 +14,6 @@ import ( lfsutil "gogs.io/gogs/internal/lfsutil" ) -// MockLFSStore is a mock implementation of the LFSStore interface (from the -// package gogs.io/gogs/internal/database) used for unit testing. -type MockLFSStore struct { - // CreateObjectFunc is an instance of a mock function object controlling - // the behavior of the method CreateObject. - CreateObjectFunc *LFSStoreCreateObjectFunc - // GetObjectByOIDFunc is an instance of a mock function object - // controlling the behavior of the method GetObjectByOID. - GetObjectByOIDFunc *LFSStoreGetObjectByOIDFunc - // GetObjectsByOIDsFunc is an instance of a mock function object - // controlling the behavior of the method GetObjectsByOIDs. - GetObjectsByOIDsFunc *LFSStoreGetObjectsByOIDsFunc -} - -// NewMockLFSStore creates a new mock of the LFSStore interface. All methods -// return zero values for all results, unless overwritten. -func NewMockLFSStore() *MockLFSStore { - return &MockLFSStore{ - CreateObjectFunc: &LFSStoreCreateObjectFunc{ - defaultHook: func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) (r0 error) { - return - }, - }, - GetObjectByOIDFunc: &LFSStoreGetObjectByOIDFunc{ - defaultHook: func(context.Context, int64, lfsutil.OID) (r0 *database.LFSObject, r1 error) { - return - }, - }, - GetObjectsByOIDsFunc: &LFSStoreGetObjectsByOIDsFunc{ - defaultHook: func(context.Context, int64, ...lfsutil.OID) (r0 []*database.LFSObject, r1 error) { - return - }, - }, - } -} - -// NewStrictMockLFSStore creates a new mock of the LFSStore interface. All -// methods panic on invocation, unless overwritten. -func NewStrictMockLFSStore() *MockLFSStore { - return &MockLFSStore{ - CreateObjectFunc: &LFSStoreCreateObjectFunc{ - defaultHook: func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error { - panic("unexpected invocation of MockLFSStore.CreateObject") - }, - }, - GetObjectByOIDFunc: &LFSStoreGetObjectByOIDFunc{ - defaultHook: func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) { - panic("unexpected invocation of MockLFSStore.GetObjectByOID") - }, - }, - GetObjectsByOIDsFunc: &LFSStoreGetObjectsByOIDsFunc{ - defaultHook: func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) { - panic("unexpected invocation of MockLFSStore.GetObjectsByOIDs") - }, - }, - } -} - -// NewMockLFSStoreFrom creates a new mock of the MockLFSStore interface. All -// methods delegate to the given implementation, unless overwritten. -func NewMockLFSStoreFrom(i database.LFSStore) *MockLFSStore { - return &MockLFSStore{ - CreateObjectFunc: &LFSStoreCreateObjectFunc{ - defaultHook: i.CreateObject, - }, - GetObjectByOIDFunc: &LFSStoreGetObjectByOIDFunc{ - defaultHook: i.GetObjectByOID, - }, - GetObjectsByOIDsFunc: &LFSStoreGetObjectsByOIDsFunc{ - defaultHook: i.GetObjectsByOIDs, - }, - } -} - -// LFSStoreCreateObjectFunc describes the behavior when the CreateObject -// method of the parent MockLFSStore instance is invoked. -type LFSStoreCreateObjectFunc struct { - defaultHook func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error - hooks []func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error - history []LFSStoreCreateObjectFuncCall - mutex sync.Mutex -} - -// CreateObject delegates to the next hook function in the queue and stores -// the parameter and result values of this invocation. -func (m *MockLFSStore) CreateObject(v0 context.Context, v1 int64, v2 lfsutil.OID, v3 int64, v4 lfsutil.Storage) error { - r0 := m.CreateObjectFunc.nextHook()(v0, v1, v2, v3, v4) - m.CreateObjectFunc.appendCall(LFSStoreCreateObjectFuncCall{v0, v1, v2, v3, v4, r0}) - return r0 -} - -// SetDefaultHook sets function that is called when the CreateObject method -// of the parent MockLFSStore instance is invoked and the hook queue is -// empty. -func (f *LFSStoreCreateObjectFunc) SetDefaultHook(hook func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error) { - f.defaultHook = hook -} - -// PushHook adds a function to the end of hook queue. Each invocation of the -// CreateObject method of the parent MockLFSStore instance invokes the hook -// at the front of the queue and discards it. After the queue is empty, the -// default hook function is invoked for any future action. -func (f *LFSStoreCreateObjectFunc) PushHook(hook func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error) { - f.mutex.Lock() - f.hooks = append(f.hooks, hook) - f.mutex.Unlock() -} - -// SetDefaultReturn calls SetDefaultHook with a function that returns the -// given values. -func (f *LFSStoreCreateObjectFunc) SetDefaultReturn(r0 error) { - f.SetDefaultHook(func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error { - return r0 - }) -} - -// PushReturn calls PushHook with a function that returns the given values. -func (f *LFSStoreCreateObjectFunc) PushReturn(r0 error) { - f.PushHook(func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error { - return r0 - }) -} - -func (f *LFSStoreCreateObjectFunc) nextHook() func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error { - f.mutex.Lock() - defer f.mutex.Unlock() - - if len(f.hooks) == 0 { - return f.defaultHook - } - - hook := f.hooks[0] - f.hooks = f.hooks[1:] - return hook -} - -func (f *LFSStoreCreateObjectFunc) appendCall(r0 LFSStoreCreateObjectFuncCall) { - f.mutex.Lock() - f.history = append(f.history, r0) - f.mutex.Unlock() -} - -// History returns a sequence of LFSStoreCreateObjectFuncCall objects -// describing the invocations of this function. -func (f *LFSStoreCreateObjectFunc) History() []LFSStoreCreateObjectFuncCall { - f.mutex.Lock() - history := make([]LFSStoreCreateObjectFuncCall, len(f.history)) - copy(history, f.history) - f.mutex.Unlock() - - return history -} - -// LFSStoreCreateObjectFuncCall is an object that describes an invocation of -// method CreateObject on an instance of MockLFSStore. -type LFSStoreCreateObjectFuncCall struct { - // Arg0 is the value of the 1st argument passed to this method - // invocation. - Arg0 context.Context - // Arg1 is the value of the 2nd argument passed to this method - // invocation. - Arg1 int64 - // Arg2 is the value of the 3rd argument passed to this method - // invocation. - Arg2 lfsutil.OID - // Arg3 is the value of the 4th argument passed to this method - // invocation. - Arg3 int64 - // Arg4 is the value of the 5th argument passed to this method - // invocation. - Arg4 lfsutil.Storage - // Result0 is the value of the 1st result returned from this method - // invocation. - Result0 error -} - -// Args returns an interface slice containing the arguments of this -// invocation. -func (c LFSStoreCreateObjectFuncCall) Args() []interface{} { - return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3, c.Arg4} -} - -// Results returns an interface slice containing the results of this -// invocation. -func (c LFSStoreCreateObjectFuncCall) Results() []interface{} { - return []interface{}{c.Result0} -} - -// LFSStoreGetObjectByOIDFunc describes the behavior when the GetObjectByOID -// method of the parent MockLFSStore instance is invoked. -type LFSStoreGetObjectByOIDFunc struct { - defaultHook func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) - hooks []func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) - history []LFSStoreGetObjectByOIDFuncCall - mutex sync.Mutex -} - -// GetObjectByOID delegates to the next hook function in the queue and -// stores the parameter and result values of this invocation. -func (m *MockLFSStore) GetObjectByOID(v0 context.Context, v1 int64, v2 lfsutil.OID) (*database.LFSObject, error) { - r0, r1 := m.GetObjectByOIDFunc.nextHook()(v0, v1, v2) - m.GetObjectByOIDFunc.appendCall(LFSStoreGetObjectByOIDFuncCall{v0, v1, v2, r0, r1}) - return r0, r1 -} - -// SetDefaultHook sets function that is called when the GetObjectByOID -// method of the parent MockLFSStore instance is invoked and the hook queue -// is empty. -func (f *LFSStoreGetObjectByOIDFunc) SetDefaultHook(hook func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error)) { - f.defaultHook = hook -} - -// PushHook adds a function to the end of hook queue. Each invocation of the -// GetObjectByOID method of the parent MockLFSStore instance invokes the -// hook at the front of the queue and discards it. After the queue is empty, -// the default hook function is invoked for any future action. -func (f *LFSStoreGetObjectByOIDFunc) PushHook(hook func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error)) { - f.mutex.Lock() - f.hooks = append(f.hooks, hook) - f.mutex.Unlock() -} - -// SetDefaultReturn calls SetDefaultHook with a function that returns the -// given values. -func (f *LFSStoreGetObjectByOIDFunc) SetDefaultReturn(r0 *database.LFSObject, r1 error) { - f.SetDefaultHook(func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) { - return r0, r1 - }) -} - -// PushReturn calls PushHook with a function that returns the given values. -func (f *LFSStoreGetObjectByOIDFunc) PushReturn(r0 *database.LFSObject, r1 error) { - f.PushHook(func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) { - return r0, r1 - }) -} - -func (f *LFSStoreGetObjectByOIDFunc) nextHook() func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) { - f.mutex.Lock() - defer f.mutex.Unlock() - - if len(f.hooks) == 0 { - return f.defaultHook - } - - hook := f.hooks[0] - f.hooks = f.hooks[1:] - return hook -} - -func (f *LFSStoreGetObjectByOIDFunc) appendCall(r0 LFSStoreGetObjectByOIDFuncCall) { - f.mutex.Lock() - f.history = append(f.history, r0) - f.mutex.Unlock() -} - -// History returns a sequence of LFSStoreGetObjectByOIDFuncCall objects -// describing the invocations of this function. -func (f *LFSStoreGetObjectByOIDFunc) History() []LFSStoreGetObjectByOIDFuncCall { - f.mutex.Lock() - history := make([]LFSStoreGetObjectByOIDFuncCall, len(f.history)) - copy(history, f.history) - f.mutex.Unlock() - - return history -} - -// LFSStoreGetObjectByOIDFuncCall is an object that describes an invocation -// of method GetObjectByOID on an instance of MockLFSStore. -type LFSStoreGetObjectByOIDFuncCall struct { - // Arg0 is the value of the 1st argument passed to this method - // invocation. - Arg0 context.Context - // Arg1 is the value of the 2nd argument passed to this method - // invocation. - Arg1 int64 - // Arg2 is the value of the 3rd argument passed to this method - // invocation. - Arg2 lfsutil.OID - // Result0 is the value of the 1st result returned from this method - // invocation. - Result0 *database.LFSObject - // Result1 is the value of the 2nd result returned from this method - // invocation. - Result1 error -} - -// Args returns an interface slice containing the arguments of this -// invocation. -func (c LFSStoreGetObjectByOIDFuncCall) Args() []interface{} { - return []interface{}{c.Arg0, c.Arg1, c.Arg2} -} - -// Results returns an interface slice containing the results of this -// invocation. -func (c LFSStoreGetObjectByOIDFuncCall) Results() []interface{} { - return []interface{}{c.Result0, c.Result1} -} - -// LFSStoreGetObjectsByOIDsFunc describes the behavior when the -// GetObjectsByOIDs method of the parent MockLFSStore instance is invoked. -type LFSStoreGetObjectsByOIDsFunc struct { - defaultHook func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) - hooks []func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) - history []LFSStoreGetObjectsByOIDsFuncCall - mutex sync.Mutex -} - -// GetObjectsByOIDs delegates to the next hook function in the queue and -// stores the parameter and result values of this invocation. -func (m *MockLFSStore) GetObjectsByOIDs(v0 context.Context, v1 int64, v2 ...lfsutil.OID) ([]*database.LFSObject, error) { - r0, r1 := m.GetObjectsByOIDsFunc.nextHook()(v0, v1, v2...) - m.GetObjectsByOIDsFunc.appendCall(LFSStoreGetObjectsByOIDsFuncCall{v0, v1, v2, r0, r1}) - return r0, r1 -} - -// SetDefaultHook sets function that is called when the GetObjectsByOIDs -// method of the parent MockLFSStore instance is invoked and the hook queue -// is empty. -func (f *LFSStoreGetObjectsByOIDsFunc) SetDefaultHook(hook func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error)) { - f.defaultHook = hook -} - -// PushHook adds a function to the end of hook queue. Each invocation of the -// GetObjectsByOIDs method of the parent MockLFSStore instance invokes the -// hook at the front of the queue and discards it. After the queue is empty, -// the default hook function is invoked for any future action. -func (f *LFSStoreGetObjectsByOIDsFunc) PushHook(hook func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error)) { - f.mutex.Lock() - f.hooks = append(f.hooks, hook) - f.mutex.Unlock() -} - -// SetDefaultReturn calls SetDefaultHook with a function that returns the -// given values. -func (f *LFSStoreGetObjectsByOIDsFunc) SetDefaultReturn(r0 []*database.LFSObject, r1 error) { - f.SetDefaultHook(func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) { - return r0, r1 - }) -} - -// PushReturn calls PushHook with a function that returns the given values. -func (f *LFSStoreGetObjectsByOIDsFunc) PushReturn(r0 []*database.LFSObject, r1 error) { - f.PushHook(func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) { - return r0, r1 - }) -} - -func (f *LFSStoreGetObjectsByOIDsFunc) nextHook() func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) { - f.mutex.Lock() - defer f.mutex.Unlock() - - if len(f.hooks) == 0 { - return f.defaultHook - } - - hook := f.hooks[0] - f.hooks = f.hooks[1:] - return hook -} - -func (f *LFSStoreGetObjectsByOIDsFunc) appendCall(r0 LFSStoreGetObjectsByOIDsFuncCall) { - f.mutex.Lock() - f.history = append(f.history, r0) - f.mutex.Unlock() -} - -// History returns a sequence of LFSStoreGetObjectsByOIDsFuncCall objects -// describing the invocations of this function. -func (f *LFSStoreGetObjectsByOIDsFunc) History() []LFSStoreGetObjectsByOIDsFuncCall { - f.mutex.Lock() - history := make([]LFSStoreGetObjectsByOIDsFuncCall, len(f.history)) - copy(history, f.history) - f.mutex.Unlock() - - return history -} - -// LFSStoreGetObjectsByOIDsFuncCall is an object that describes an -// invocation of method GetObjectsByOIDs on an instance of MockLFSStore. -type LFSStoreGetObjectsByOIDsFuncCall struct { - // Arg0 is the value of the 1st argument passed to this method - // invocation. - Arg0 context.Context - // Arg1 is the value of the 2nd argument passed to this method - // invocation. - Arg1 int64 - // Arg2 is a slice containing the values of the variadic arguments - // passed to this method invocation. - Arg2 []lfsutil.OID - // Result0 is the value of the 1st result returned from this method - // invocation. - Result0 []*database.LFSObject - // Result1 is the value of the 2nd result returned from this method - // invocation. - Result1 error -} - -// Args returns an interface slice containing the arguments of this -// invocation. The variadic slice argument is flattened in this array such -// that one positional argument and three variadic arguments would result in -// a slice of four, not two. -func (c LFSStoreGetObjectsByOIDsFuncCall) Args() []interface{} { - trailing := []interface{}{} - for _, val := range c.Arg2 { - trailing = append(trailing, val) - } - - return append([]interface{}{c.Arg0, c.Arg1}, trailing...) -} - -// Results returns an interface slice containing the results of this -// invocation. -func (c LFSStoreGetObjectsByOIDsFuncCall) Results() []interface{} { - return []interface{}{c.Result0, c.Result1} -} - // MockPermsStore is a mock implementation of the PermsStore interface (from // the package gogs.io/gogs/internal/database) used for unit testing. type MockPermsStore struct { @@ -6051,9 +5634,18 @@ func (c UsersStoreUseCustomAvatarFuncCall) Results() []interface{} { // MockStore is a mock implementation of the Store interface (from the // package gogs.io/gogs/internal/route/lfs) used for unit testing. type MockStore struct { + // CreateLFSObjectFunc is an instance of a mock function object + // controlling the behavior of the method CreateLFSObject. + CreateLFSObjectFunc *StoreCreateLFSObjectFunc // GetAccessTokenBySHA1Func is an instance of a mock function object // controlling the behavior of the method GetAccessTokenBySHA1. GetAccessTokenBySHA1Func *StoreGetAccessTokenBySHA1Func + // GetLFSObjectByOIDFunc is an instance of a mock function object + // controlling the behavior of the method GetLFSObjectByOID. + GetLFSObjectByOIDFunc *StoreGetLFSObjectByOIDFunc + // GetLFSObjectsByOIDsFunc is an instance of a mock function object + // controlling the behavior of the method GetLFSObjectsByOIDs. + GetLFSObjectsByOIDsFunc *StoreGetLFSObjectsByOIDsFunc // TouchAccessTokenByIDFunc is an instance of a mock function object // controlling the behavior of the method TouchAccessTokenByID. TouchAccessTokenByIDFunc *StoreTouchAccessTokenByIDFunc @@ -6063,11 +5655,26 @@ type MockStore struct { // return zero values for all results, unless overwritten. func NewMockStore() *MockStore { return &MockStore{ + CreateLFSObjectFunc: &StoreCreateLFSObjectFunc{ + defaultHook: func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) (r0 error) { + return + }, + }, GetAccessTokenBySHA1Func: &StoreGetAccessTokenBySHA1Func{ defaultHook: func(context.Context, string) (r0 *database.AccessToken, r1 error) { return }, }, + GetLFSObjectByOIDFunc: &StoreGetLFSObjectByOIDFunc{ + defaultHook: func(context.Context, int64, lfsutil.OID) (r0 *database.LFSObject, r1 error) { + return + }, + }, + GetLFSObjectsByOIDsFunc: &StoreGetLFSObjectsByOIDsFunc{ + defaultHook: func(context.Context, int64, ...lfsutil.OID) (r0 []*database.LFSObject, r1 error) { + return + }, + }, TouchAccessTokenByIDFunc: &StoreTouchAccessTokenByIDFunc{ defaultHook: func(context.Context, int64) (r0 error) { return @@ -6080,11 +5687,26 @@ func NewMockStore() *MockStore { // panic on invocation, unless overwritten. func NewStrictMockStore() *MockStore { return &MockStore{ + CreateLFSObjectFunc: &StoreCreateLFSObjectFunc{ + defaultHook: func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error { + panic("unexpected invocation of MockStore.CreateLFSObject") + }, + }, GetAccessTokenBySHA1Func: &StoreGetAccessTokenBySHA1Func{ defaultHook: func(context.Context, string) (*database.AccessToken, error) { panic("unexpected invocation of MockStore.GetAccessTokenBySHA1") }, }, + GetLFSObjectByOIDFunc: &StoreGetLFSObjectByOIDFunc{ + defaultHook: func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) { + panic("unexpected invocation of MockStore.GetLFSObjectByOID") + }, + }, + GetLFSObjectsByOIDsFunc: &StoreGetLFSObjectsByOIDsFunc{ + defaultHook: func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) { + panic("unexpected invocation of MockStore.GetLFSObjectsByOIDs") + }, + }, TouchAccessTokenByIDFunc: &StoreTouchAccessTokenByIDFunc{ defaultHook: func(context.Context, int64) error { panic("unexpected invocation of MockStore.TouchAccessTokenByID") @@ -6097,15 +5719,138 @@ func NewStrictMockStore() *MockStore { // methods delegate to the given implementation, unless overwritten. func NewMockStoreFrom(i Store) *MockStore { return &MockStore{ + CreateLFSObjectFunc: &StoreCreateLFSObjectFunc{ + defaultHook: i.CreateLFSObject, + }, GetAccessTokenBySHA1Func: &StoreGetAccessTokenBySHA1Func{ defaultHook: i.GetAccessTokenBySHA1, }, + GetLFSObjectByOIDFunc: &StoreGetLFSObjectByOIDFunc{ + defaultHook: i.GetLFSObjectByOID, + }, + GetLFSObjectsByOIDsFunc: &StoreGetLFSObjectsByOIDsFunc{ + defaultHook: i.GetLFSObjectsByOIDs, + }, TouchAccessTokenByIDFunc: &StoreTouchAccessTokenByIDFunc{ defaultHook: i.TouchAccessTokenByID, }, } } +// StoreCreateLFSObjectFunc describes the behavior when the CreateLFSObject +// method of the parent MockStore instance is invoked. +type StoreCreateLFSObjectFunc struct { + defaultHook func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error + hooks []func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error + history []StoreCreateLFSObjectFuncCall + mutex sync.Mutex +} + +// CreateLFSObject delegates to the next hook function in the queue and +// stores the parameter and result values of this invocation. +func (m *MockStore) CreateLFSObject(v0 context.Context, v1 int64, v2 lfsutil.OID, v3 int64, v4 lfsutil.Storage) error { + r0 := m.CreateLFSObjectFunc.nextHook()(v0, v1, v2, v3, v4) + m.CreateLFSObjectFunc.appendCall(StoreCreateLFSObjectFuncCall{v0, v1, v2, v3, v4, r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the CreateLFSObject +// method of the parent MockStore instance is invoked and the hook queue is +// empty. +func (f *StoreCreateLFSObjectFunc) SetDefaultHook(hook func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// CreateLFSObject method of the parent MockStore instance invokes the hook +// at the front of the queue and discards it. After the queue is empty, the +// default hook function is invoked for any future action. +func (f *StoreCreateLFSObjectFunc) PushHook(hook func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *StoreCreateLFSObjectFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *StoreCreateLFSObjectFunc) PushReturn(r0 error) { + f.PushHook(func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error { + return r0 + }) +} + +func (f *StoreCreateLFSObjectFunc) nextHook() func(context.Context, int64, lfsutil.OID, int64, lfsutil.Storage) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *StoreCreateLFSObjectFunc) appendCall(r0 StoreCreateLFSObjectFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of StoreCreateLFSObjectFuncCall objects +// describing the invocations of this function. +func (f *StoreCreateLFSObjectFunc) History() []StoreCreateLFSObjectFuncCall { + f.mutex.Lock() + history := make([]StoreCreateLFSObjectFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// StoreCreateLFSObjectFuncCall is an object that describes an invocation of +// method CreateLFSObject on an instance of MockStore. +type StoreCreateLFSObjectFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 int64 + // Arg2 is the value of the 3rd argument passed to this method + // invocation. + Arg2 lfsutil.OID + // Arg3 is the value of the 4th argument passed to this method + // invocation. + Arg3 int64 + // Arg4 is the value of the 5th argument passed to this method + // invocation. + Arg4 lfsutil.Storage + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c StoreCreateLFSObjectFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3, c.Arg4} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c StoreCreateLFSObjectFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + // StoreGetAccessTokenBySHA1Func describes the behavior when the // GetAccessTokenBySHA1 method of the parent MockStore instance is invoked. type StoreGetAccessTokenBySHA1Func struct { @@ -6214,6 +5959,235 @@ func (c StoreGetAccessTokenBySHA1FuncCall) Results() []interface{} { return []interface{}{c.Result0, c.Result1} } +// StoreGetLFSObjectByOIDFunc describes the behavior when the +// GetLFSObjectByOID method of the parent MockStore instance is invoked. +type StoreGetLFSObjectByOIDFunc struct { + defaultHook func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) + hooks []func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) + history []StoreGetLFSObjectByOIDFuncCall + mutex sync.Mutex +} + +// GetLFSObjectByOID delegates to the next hook function in the queue and +// stores the parameter and result values of this invocation. +func (m *MockStore) GetLFSObjectByOID(v0 context.Context, v1 int64, v2 lfsutil.OID) (*database.LFSObject, error) { + r0, r1 := m.GetLFSObjectByOIDFunc.nextHook()(v0, v1, v2) + m.GetLFSObjectByOIDFunc.appendCall(StoreGetLFSObjectByOIDFuncCall{v0, v1, v2, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the GetLFSObjectByOID +// method of the parent MockStore instance is invoked and the hook queue is +// empty. +func (f *StoreGetLFSObjectByOIDFunc) SetDefaultHook(hook func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// GetLFSObjectByOID method of the parent MockStore instance invokes the +// hook at the front of the queue and discards it. After the queue is empty, +// the default hook function is invoked for any future action. +func (f *StoreGetLFSObjectByOIDFunc) PushHook(hook func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *StoreGetLFSObjectByOIDFunc) SetDefaultReturn(r0 *database.LFSObject, r1 error) { + f.SetDefaultHook(func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *StoreGetLFSObjectByOIDFunc) PushReturn(r0 *database.LFSObject, r1 error) { + f.PushHook(func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) { + return r0, r1 + }) +} + +func (f *StoreGetLFSObjectByOIDFunc) nextHook() func(context.Context, int64, lfsutil.OID) (*database.LFSObject, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *StoreGetLFSObjectByOIDFunc) appendCall(r0 StoreGetLFSObjectByOIDFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of StoreGetLFSObjectByOIDFuncCall objects +// describing the invocations of this function. +func (f *StoreGetLFSObjectByOIDFunc) History() []StoreGetLFSObjectByOIDFuncCall { + f.mutex.Lock() + history := make([]StoreGetLFSObjectByOIDFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// StoreGetLFSObjectByOIDFuncCall is an object that describes an invocation +// of method GetLFSObjectByOID on an instance of MockStore. +type StoreGetLFSObjectByOIDFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 int64 + // Arg2 is the value of the 3rd argument passed to this method + // invocation. + Arg2 lfsutil.OID + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 *database.LFSObject + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c StoreGetLFSObjectByOIDFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1, c.Arg2} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c StoreGetLFSObjectByOIDFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + +// StoreGetLFSObjectsByOIDsFunc describes the behavior when the +// GetLFSObjectsByOIDs method of the parent MockStore instance is invoked. +type StoreGetLFSObjectsByOIDsFunc struct { + defaultHook func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) + hooks []func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) + history []StoreGetLFSObjectsByOIDsFuncCall + mutex sync.Mutex +} + +// GetLFSObjectsByOIDs delegates to the next hook function in the queue and +// stores the parameter and result values of this invocation. +func (m *MockStore) GetLFSObjectsByOIDs(v0 context.Context, v1 int64, v2 ...lfsutil.OID) ([]*database.LFSObject, error) { + r0, r1 := m.GetLFSObjectsByOIDsFunc.nextHook()(v0, v1, v2...) + m.GetLFSObjectsByOIDsFunc.appendCall(StoreGetLFSObjectsByOIDsFuncCall{v0, v1, v2, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the GetLFSObjectsByOIDs +// method of the parent MockStore instance is invoked and the hook queue is +// empty. +func (f *StoreGetLFSObjectsByOIDsFunc) SetDefaultHook(hook func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// GetLFSObjectsByOIDs method of the parent MockStore instance invokes the +// hook at the front of the queue and discards it. After the queue is empty, +// the default hook function is invoked for any future action. +func (f *StoreGetLFSObjectsByOIDsFunc) PushHook(hook func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *StoreGetLFSObjectsByOIDsFunc) SetDefaultReturn(r0 []*database.LFSObject, r1 error) { + f.SetDefaultHook(func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *StoreGetLFSObjectsByOIDsFunc) PushReturn(r0 []*database.LFSObject, r1 error) { + f.PushHook(func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) { + return r0, r1 + }) +} + +func (f *StoreGetLFSObjectsByOIDsFunc) nextHook() func(context.Context, int64, ...lfsutil.OID) ([]*database.LFSObject, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *StoreGetLFSObjectsByOIDsFunc) appendCall(r0 StoreGetLFSObjectsByOIDsFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of StoreGetLFSObjectsByOIDsFuncCall objects +// describing the invocations of this function. +func (f *StoreGetLFSObjectsByOIDsFunc) History() []StoreGetLFSObjectsByOIDsFuncCall { + f.mutex.Lock() + history := make([]StoreGetLFSObjectsByOIDsFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// StoreGetLFSObjectsByOIDsFuncCall is an object that describes an +// invocation of method GetLFSObjectsByOIDs on an instance of MockStore. +type StoreGetLFSObjectsByOIDsFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 int64 + // Arg2 is a slice containing the values of the variadic arguments + // passed to this method invocation. + Arg2 []lfsutil.OID + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 []*database.LFSObject + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. The variadic slice argument is flattened in this array such +// that one positional argument and three variadic arguments would result in +// a slice of four, not two. +func (c StoreGetLFSObjectsByOIDsFuncCall) Args() []interface{} { + trailing := []interface{}{} + for _, val := range c.Arg2 { + trailing = append(trailing, val) + } + + return append([]interface{}{c.Arg0, c.Arg1}, trailing...) +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c StoreGetLFSObjectsByOIDsFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + // StoreTouchAccessTokenByIDFunc describes the behavior when the // TouchAccessTokenByID method of the parent MockStore instance is invoked. type StoreTouchAccessTokenByIDFunc struct { diff --git a/internal/route/lfs/route.go b/internal/route/lfs/route.go index 9ddff3e6b..cdac39e6c 100644 --- a/internal/route/lfs/route.go +++ b/internal/route/lfs/route.go @@ -28,9 +28,10 @@ func RegisterRoutes(r *macaron.Router) { store := NewStore() r.Group("", func() { - r.Post("/objects/batch", authorize(database.AccessModeRead), verifyAccept, verifyContentTypeJSON, serveBatch) + r.Post("/objects/batch", authorize(database.AccessModeRead), verifyAccept, verifyContentTypeJSON, serveBatch(store)) r.Group("/objects/basic", func() { basic := &basicHandler{ + store: store, defaultStorage: lfsutil.Storage(conf.LFS.Storage), storagers: map[lfsutil.Storage]lfsutil.Storager{ lfsutil.StorageLocal: &lfsutil.LocalStorage{Root: conf.LFS.ObjectsPath}, diff --git a/internal/route/lfs/store.go b/internal/route/lfs/store.go index 1a9bf3d3e..ffd9ae5e2 100644 --- a/internal/route/lfs/store.go +++ b/internal/route/lfs/store.go @@ -4,6 +4,7 @@ import ( "context" "gogs.io/gogs/internal/database" + "gogs.io/gogs/internal/lfsutil" ) // Store is the data layer carrier for LFS endpoints. This interface is meant to @@ -16,6 +17,15 @@ type Store interface { // TouchAccessTokenByID updates the updated time of the given access token to // the current time. TouchAccessTokenByID(ctx context.Context, id int64) error + + // CreateLFSObject creates an LFS object record in database. + CreateLFSObject(ctx context.Context, repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error + // GetLFSObjectByOID returns the LFS object with given OID. It returns + // database.ErrLFSObjectNotExist when not found. + GetLFSObjectByOID(ctx context.Context, repoID int64, oid lfsutil.OID) (*database.LFSObject, error) + // GetLFSObjectsByOIDs returns LFS objects found within "oids". The returned + // list could have fewer elements if some oids were not found. + GetLFSObjectsByOIDs(ctx context.Context, repoID int64, oids ...lfsutil.OID) ([]*database.LFSObject, error) } type store struct{} @@ -32,3 +42,15 @@ func (*store) GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database. func (*store) TouchAccessTokenByID(ctx context.Context, id int64) error { return database.Handle.AccessTokens().Touch(ctx, id) } + +func (*store) CreateLFSObject(ctx context.Context, repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error { + return database.Handle.LFS().CreateObject(ctx, repoID, oid, size, storage) +} + +func (*store) GetLFSObjectByOID(ctx context.Context, repoID int64, oid lfsutil.OID) (*database.LFSObject, error) { + return database.Handle.LFS().GetObjectByOID(ctx, repoID, oid) +} + +func (*store) GetLFSObjectsByOIDs(ctx context.Context, repoID int64, oids ...lfsutil.OID) ([]*database.LFSObject, error) { + return database.Handle.LFS().GetObjectsByOIDs(ctx, repoID, oids...) +} diff --git a/mockgen.yaml b/mockgen.yaml index 74bec0153..f5653ea1e 100644 --- a/mockgen.yaml +++ b/mockgen.yaml @@ -36,7 +36,6 @@ mocks: sources: - path: gogs.io/gogs/internal/database interfaces: - - LFSStore - UsersStore - TwoFactorsStore - ReposStore