lfsutil: add `Storager` interface and local storage (#6083)

* Add Storager interface

* Add tests

* Add back note

* Add tests for basic protocol routes

* Fix lint errors
pull/6084/head
ᴜɴᴋɴᴡᴏɴ 2020-04-10 22:13:42 +08:00 committed by GitHub
parent 3e055e329c
commit 9a5b227f3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 620 additions and 88 deletions

View File

@ -264,7 +264,9 @@ HOST =
ACCESS_CONTROL_ALLOW_ORIGIN =
[lfs]
; The root path to store LFS objects.
; The storage backend for uploading new objects.
STORAGE = local
; The root path to store LFS objects on local file system.
OBJECTS_PATH = data/lfs-objects
[attachment]

File diff suppressed because one or more lines are too long

View File

@ -193,11 +193,6 @@ var (
AccessControlAllowOrigin string
}
// LFS settings
LFS struct {
ObjectsPath string
}
// Attachment settings
Attachment struct {
Enabled bool
@ -417,6 +412,14 @@ type DatabaseOpts struct {
// Database settings
var Database DatabaseOpts
type LFSOpts struct {
Storage string
ObjectsPath string
}
// LFS settings
var LFS LFSOpts
type i18nConf struct {
Langs []string `delim:","`
Names []string `delim:","`

View File

@ -6,15 +6,10 @@ package db
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/errutil"
"gogs.io/gogs/internal/lfsutil"
)
@ -23,8 +18,8 @@ import (
//
// NOTE: All methods are sorted in alphabetical order.
type LFSStore interface {
// CreateObject streams io.ReadCloser to target storage and creates a record in database.
CreateObject(repoID int64, oid lfsutil.OID, rc io.ReadCloser, storage lfsutil.Storage) error
// CreateObject creates a LFS object record in database.
CreateObject(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(repoID int64, oid lfsutil.OID) (*LFSObject, error)
@ -48,44 +43,11 @@ type LFSObject struct {
CreatedAt time.Time `gorm:"NOT NULL"`
}
func (db *lfs) CreateObject(repoID int64, oid lfsutil.OID, rc io.ReadCloser, storage lfsutil.Storage) error {
if storage != lfsutil.StorageLocal {
return errors.New("only local storage is supported")
}
var ioerr error
fpath := lfsutil.StorageLocalPath(conf.LFS.ObjectsPath, oid)
defer func() {
rc.Close()
// NOTE: Only remove the file if there is an IO error, 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 same file as OID
// is seen as unique to every file.
if ioerr != nil {
_ = os.Remove(fpath)
}
}()
ioerr = os.MkdirAll(filepath.Dir(fpath), os.ModePerm)
if ioerr != nil {
return errors.Wrap(ioerr, "create directories")
}
w, ioerr := os.Create(fpath)
if ioerr != nil {
return errors.Wrap(ioerr, "create file")
}
defer w.Close()
written, ioerr := io.Copy(w, rc)
if ioerr != nil {
return errors.Wrap(ioerr, "copy file")
}
func (db *lfs) CreateObject(repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error {
object := &LFSObject{
RepoID: repoID,
OID: oid,
Size: written,
Size: size,
Storage: storage,
}
return db.DB.Create(object).Error

View File

@ -5,7 +5,6 @@
package db
import (
"io"
"testing"
"gogs.io/gogs/internal/lfsutil"
@ -39,13 +38,13 @@ func SetMockAccessTokensStore(t *testing.T, mock AccessTokensStore) {
var _ LFSStore = (*MockLFSStore)(nil)
type MockLFSStore struct {
MockCreateObject func(repoID int64, oid lfsutil.OID, rc io.ReadCloser, storage lfsutil.Storage) error
MockCreateObject func(repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error
MockGetObjectByOID func(repoID int64, oid lfsutil.OID) (*LFSObject, error)
MockGetObjectsByOIDs func(repoID int64, oids ...lfsutil.OID) ([]*LFSObject, error)
}
func (m *MockLFSStore) CreateObject(repoID int64, oid lfsutil.OID, rc io.ReadCloser, storage lfsutil.Storage) error {
return m.MockCreateObject(repoID, oid, rc, storage)
func (m *MockLFSStore) CreateObject(repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error {
return m.MockCreateObject(repoID, oid, size, storage)
}
func (m *MockLFSStore) GetObjectByOID(repoID int64, oid lfsutil.OID) (*LFSObject, error) {

View File

@ -5,6 +5,8 @@
package lfsutil
import (
"github.com/pkg/errors"
"gogs.io/gogs/internal/lazyregexp"
)
@ -15,6 +17,8 @@ type OID string
// Spec: https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
var oidRe = lazyregexp.New("^[a-f0-9]{64}$")
var ErrInvalidOID = errors.New("OID is not valid")
// ValidOID returns true if given oid is valid.
func ValidOID(oid OID) bool {
return oidRe.MatchString(string(oid))

View File

@ -5,9 +5,31 @@
package lfsutil
import (
"io"
"os"
"path/filepath"
"github.com/pkg/errors"
"gogs.io/gogs/internal/osutil"
)
var ErrObjectNotExist = errors.New("Object does not exist")
// Storager is an storage backend for uploading and downloading LFS objects.
type Storager interface {
// Storage returns the name of the storage backend.
Storage() Storage
// Upload reads content from the io.ReadCloser and uploads as given oid.
// The reader is closed once upload is finished. ErrInvalidOID is returned
// if the given oid is not valid.
Upload(oid OID, rc io.ReadCloser) (int64, error)
// Download streams content of given oid to the io.Writer. It is caller's
// responsibility the close the writer when needed. ErrObjectNotExist is
// returned if the given oid does not exist.
Download(oid OID, w io.Writer) error
}
// Storage is the storage type of an LFS object.
type Storage string
@ -15,12 +37,73 @@ const (
StorageLocal Storage = "local"
)
// StorageLocalPath returns computed file path for storing object on local file system.
// It returns empty string if given "oid" isn't valid.
func StorageLocalPath(root string, oid OID) string {
if !ValidOID(oid) {
var _ Storager = (*LocalStorage)(nil)
// LocalStorage is a LFS storage backend on local file system.
type LocalStorage struct {
// The root path for storing LFS objects.
Root string
}
func (s *LocalStorage) Storage() Storage {
return StorageLocal
}
func (s *LocalStorage) storagePath(oid OID) string {
if len(oid) < 2 {
return ""
}
return filepath.Join(root, string(oid[0]), string(oid[1]), string(oid))
return filepath.Join(s.Root, string(oid[0]), string(oid[1]), string(oid))
}
func (s *LocalStorage) Upload(oid OID, rc io.ReadCloser) (int64, error) {
if !ValidOID(oid) {
return 0, ErrInvalidOID
}
var err error
fpath := s.storagePath(oid)
defer func() {
rc.Close()
if err != nil {
_ = os.Remove(fpath)
}
}()
err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm)
if err != nil {
return 0, errors.Wrap(err, "create directories")
}
w, err := os.Create(fpath)
if err != nil {
return 0, errors.Wrap(err, "create file")
}
defer w.Close()
written, err := io.Copy(w, rc)
if err != nil {
return 0, errors.Wrap(err, "copy file")
}
return written, nil
}
func (s *LocalStorage) Download(oid OID, w io.Writer) error {
fpath := s.storagePath(oid)
if !osutil.IsFile(fpath) {
return ErrObjectNotExist
}
r, err := os.Open(fpath)
if err != nil {
return errors.Wrap(err, "open file")
}
defer r.Close()
_, err = io.Copy(w, r)
if err != nil {
return errors.Wrap(err, "copy file")
}
return nil
}

View File

@ -5,39 +5,130 @@
package lfsutil
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestStorageLocalPath(t *testing.T) {
func TestLocalStorage_storagePath(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping testing on Windows")
return
}
s := &LocalStorage{
Root: "/lfs-objects",
}
tests := []struct {
name string
root string
oid OID
expPath string
}{
{
name: "invalid oid",
oid: OID("bad_oid"),
name: "empty oid",
oid: "",
},
{
name: "valid oid",
root: "/lfs-objects",
oid: OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"),
oid: "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
expPath: "/lfs-objects/e/f/ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expPath, StorageLocalPath(test.root, test.oid))
assert.Equal(t, test.expPath, s.storagePath(test.oid))
})
}
}
func TestLocalStorage_Upload(t *testing.T) {
s := &LocalStorage{
Root: filepath.Join(os.TempDir(), "lfs-objects"),
}
t.Cleanup(func() {
_ = os.RemoveAll(s.Root)
})
tests := []struct {
name string
oid OID
content string
expWritten int64
expErr error
}{
{
name: "invalid oid",
oid: "bad_oid",
expErr: ErrInvalidOID,
},
{
name: "valid oid",
oid: "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
content: "Hello world!",
expWritten: 12,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
written, err := s.Upload(test.oid, ioutil.NopCloser(strings.NewReader(test.content)))
assert.Equal(t, test.expWritten, written)
assert.Equal(t, test.expErr, err)
})
}
}
func TestLocalStorage_Download(t *testing.T) {
oid := OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f")
s := &LocalStorage{
Root: filepath.Join(os.TempDir(), "lfs-objects"),
}
t.Cleanup(func() {
_ = os.RemoveAll(s.Root)
})
fpath := s.storagePath(oid)
err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm)
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fpath, []byte("Hello world!"), os.ModePerm)
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
oid OID
expContent string
expErr error
}{
{
name: "object not exists",
oid: "bad_oid",
expErr: ErrObjectNotExist,
},
{
name: "valid oid",
oid: oid,
expContent: "Hello world!",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var buf bytes.Buffer
err := s.Download(test.oid, &buf)
assert.Equal(t, test.expContent, buf.String())
assert.Equal(t, test.expErr, err)
})
}
}

View File

@ -9,13 +9,11 @@ import (
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
"gopkg.in/macaron.v1"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/lfsutil"
"gogs.io/gogs/internal/strutil"
@ -27,8 +25,25 @@ const (
basicOperationDownload = "download"
)
type basicHandler struct {
// The default storage backend for uploading new objects.
defaultStorage lfsutil.Storage
// The list of available storage backends to access objects.
storagers map[lfsutil.Storage]lfsutil.Storager
}
// DefaultStorager returns the default storage backend.
func (h *basicHandler) DefaultStorager() lfsutil.Storager {
return h.storagers[h.defaultStorage]
}
// Storager returns the given storage backend.
func (h *basicHandler) Storager(storage lfsutil.Storage) lfsutil.Storager {
return h.storagers[storage]
}
// GET /{owner}/{repo}.git/info/lfs/object/basic/{oid}
func serveBasicDownload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) {
func (h *basicHandler) serveDownload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) {
object, err := db.LFS.GetObjectByOID(repo.ID, oid)
if err != nil {
if db.IsErrLFSObjectNotExist(err) {
@ -42,28 +57,26 @@ func serveBasicDownload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID
return
}
fpath := lfsutil.StorageLocalPath(conf.LFS.ObjectsPath, object.OID)
r, err := os.Open(fpath)
if err != nil {
s := h.Storager(object.Storage)
if s == nil {
internalServerError(c.Resp)
log.Error("Failed to open object file [path: %s]: %v", fpath, err)
log.Error("Failed to locate the object [repo_id: %d, oid: %s]: storage %q not found", object.RepoID, object.OID, object.Storage)
return
}
defer r.Close()
c.Header().Set("Content-Type", "application/octet-stream")
c.Header().Set("Content-Length", strconv.FormatInt(object.Size, 10))
c.Status(http.StatusOK)
_, err = io.Copy(c.Resp, r)
err = s.Download(object.OID, c.Resp)
if err != nil {
log.Error("Failed to copy object file: %v", err)
log.Error("Failed to download object [oid: %s]: %v", object.OID, err)
return
}
}
// PUT /{owner}/{repo}.git/info/lfs/object/basic/{oid}
func serveBasicUpload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) {
func (h *basicHandler) serveUpload(c *macaron.Context, repo *db.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 := db.LFS.GetObjectByOID(repo.ID, oid)
@ -79,8 +92,25 @@ func serveBasicUpload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID)
return
}
err = db.LFS.CreateObject(repo.ID, oid, c.Req.Request.Body, lfsutil.StorageLocal)
s := h.DefaultStorager()
written, err := s.Upload(oid, c.Req.Request.Body)
if err != nil {
if err == lfsutil.ErrInvalidOID {
responseJSON(c.Resp, http.StatusBadRequest, responseError{
Message: err.Error(),
})
} else {
internalServerError(c.Resp)
log.Error("Failed to upload object [storage: %s, oid: %s]: %v", s.Storage(), oid, err)
}
return
}
err = db.LFS.CreateObject(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
// same file as OID is seen as unique to every file.
internalServerError(c.Resp)
log.Error("Failed to create object [repo_id: %d, oid: %s]: %v", repo.ID, oid, err)
return
@ -91,7 +121,7 @@ func serveBasicUpload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID)
}
// POST /{owner}/{repo}.git/info/lfs/object/basic/verify
func serveBasicVerify(c *macaron.Context, repo *db.Repository) {
func (h *basicHandler) serveVerify(c *macaron.Context, repo *db.Repository) {
var request basicVerifyRequest
defer c.Req.Request.Body.Close()
err := json.NewDecoder(c.Req.Request.Body).Decode(&request)
@ -109,7 +139,7 @@ func serveBasicVerify(c *macaron.Context, repo *db.Repository) {
return
}
object, err := db.LFS.GetObjectByOID(repo.ID, lfsutil.OID(request.Oid))
object, err := db.LFS.GetObjectByOID(repo.ID, request.Oid)
if err != nil {
if db.IsErrLFSObjectNotExist(err) {
responseJSON(c.Resp, http.StatusNotFound, responseError{
@ -123,7 +153,7 @@ func serveBasicVerify(c *macaron.Context, repo *db.Repository) {
}
if object.Size != request.Size {
responseJSON(c.Resp, http.StatusNotFound, responseError{
responseJSON(c.Resp, http.StatusBadRequest, responseError{
Message: "Object size mismatch",
})
return

View File

@ -0,0 +1,288 @@
// Copyright 2020 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package lfs
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/macaron.v1"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/lfsutil"
)
var _ lfsutil.Storager = (*mockStorage)(nil)
// mockStorage is a in-memory storage for LFS objects.
type mockStorage struct {
buf *bytes.Buffer
}
func (s *mockStorage) Storage() lfsutil.Storage {
return "memory"
}
func (s *mockStorage) Upload(oid lfsutil.OID, rc io.ReadCloser) (int64, error) {
defer rc.Close()
return io.Copy(s.buf, rc)
}
func (s *mockStorage) Download(oid lfsutil.OID, w io.Writer) error {
_, err := io.Copy(w, s.buf)
return err
}
func Test_basicHandler_serveDownload(t *testing.T) {
s := &mockStorage{}
basic := &basicHandler{
defaultStorage: s.Storage(),
storagers: map[lfsutil.Storage]lfsutil.Storager{
s.Storage(): s,
},
}
m := macaron.New()
m.Use(macaron.Renderer())
m.Use(func(c *macaron.Context) {
c.Map(&db.Repository{Name: "repo"})
c.Map(lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"))
})
m.Get("/", basic.serveDownload)
tests := []struct {
name string
content string
mockLFSStore *db.MockLFSStore
expStatusCode int
expHeader http.Header
expBody string
}{
{
name: "object does not exist",
mockLFSStore: &db.MockLFSStore{
MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
return nil, db.ErrLFSObjectNotExist{}
},
},
expStatusCode: http.StatusNotFound,
expHeader: http.Header{
"Content-Type": []string{"application/vnd.git-lfs+json"},
},
expBody: `{"message":"Object does not exist"}` + "\n",
},
{
name: "storage not found",
mockLFSStore: &db.MockLFSStore{
MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
return &db.LFSObject{Storage: "bad_storage"}, nil
},
},
expStatusCode: http.StatusInternalServerError,
expHeader: http.Header{
"Content-Type": []string{"application/vnd.git-lfs+json"},
},
expBody: `{"message":"Internal server error"}` + "\n",
},
{
name: "object exists",
content: "Hello world!",
mockLFSStore: &db.MockLFSStore{
MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
return &db.LFSObject{
Size: 12,
Storage: s.Storage(),
}, nil
},
},
expStatusCode: http.StatusOK,
expHeader: http.Header{
"Content-Type": []string{"application/octet-stream"},
"Content-Length": []string{"12"},
},
expBody: "Hello world!",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
db.SetMockLFSStore(t, test.mockLFSStore)
s.buf = bytes.NewBufferString(test.content)
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
m.ServeHTTP(rr, r)
resp := rr.Result()
assert.Equal(t, test.expStatusCode, resp.StatusCode)
assert.Equal(t, test.expHeader, resp.Header)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.expBody, string(body))
})
}
}
func Test_basicHandler_serveUpload(t *testing.T) {
s := &mockStorage{buf: &bytes.Buffer{}}
basic := &basicHandler{
defaultStorage: s.Storage(),
storagers: map[lfsutil.Storage]lfsutil.Storager{
s.Storage(): s,
},
}
m := macaron.New()
m.Use(macaron.Renderer())
m.Use(func(c *macaron.Context) {
c.Map(&db.Repository{Name: "repo"})
c.Map(lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"))
})
m.Put("/", basic.serveUpload)
tests := []struct {
name string
mockLFSStore *db.MockLFSStore
expStatusCode int
expBody string
}{
{
name: "object already exists",
mockLFSStore: &db.MockLFSStore{
MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
return &db.LFSObject{}, nil
},
},
expStatusCode: http.StatusOK,
},
{
name: "new object",
mockLFSStore: &db.MockLFSStore{
MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
return nil, db.ErrLFSObjectNotExist{}
},
MockCreateObject: func(repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error {
return nil
},
},
expStatusCode: http.StatusOK,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
db.SetMockLFSStore(t, test.mockLFSStore)
r, err := http.NewRequest("PUT", "/", strings.NewReader("Hello world!"))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
m.ServeHTTP(rr, r)
resp := rr.Result()
assert.Equal(t, test.expStatusCode, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.expBody, string(body))
})
}
}
func Test_basicHandler_serveVerify(t *testing.T) {
m := macaron.New()
m.Use(macaron.Renderer())
m.Use(func(c *macaron.Context) {
c.Map(&db.Repository{Name: "repo"})
})
m.Post("/", (&basicHandler{}).serveVerify)
tests := []struct {
name string
body string
mockLFSStore *db.MockLFSStore
expStatusCode int
expBody string
}{
{
name: "invalid oid",
body: `{"oid": "bad_oid"}`,
expStatusCode: http.StatusBadRequest,
expBody: `{"message":"Invalid oid"}` + "\n",
},
{
name: "object does not exist",
body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"}`,
mockLFSStore: &db.MockLFSStore{
MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
return nil, db.ErrLFSObjectNotExist{}
},
},
expStatusCode: http.StatusNotFound,
expBody: `{"message":"Object does not exist"}` + "\n",
},
{
name: "object size mismatch",
body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"}`,
mockLFSStore: &db.MockLFSStore{
MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
return &db.LFSObject{Size: 12}, nil
},
},
expStatusCode: http.StatusBadRequest,
expBody: `{"message":"Object size mismatch"}` + "\n",
},
{
name: "object exists",
body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f", "size":12}`,
mockLFSStore: &db.MockLFSStore{
MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
return &db.LFSObject{Size: 12}, nil
},
},
expStatusCode: http.StatusOK,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
db.SetMockLFSStore(t, test.mockLFSStore)
r, err := http.NewRequest("POST", "/", strings.NewReader(test.body))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
m.ServeHTTP(rr, r)
resp := rr.Result()
assert.Equal(t, test.expStatusCode, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.expBody, string(body))
})
}
}

View File

@ -23,6 +23,7 @@ func Test_serveBatch(t *testing.T) {
conf.SetMockServer(t, conf.ServerOpts{
ExternalURL: "https://gogs.example.com/",
})
m := macaron.New()
m.Use(func(c *macaron.Context) {
c.Map(&db.User{Name: "owner"})

View File

@ -0,0 +1,30 @@
// Copyright 2020 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package lfs
import (
"flag"
"fmt"
"os"
"testing"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/testutil"
)
func TestMain(m *testing.M) {
flag.Parse()
if !testing.Verbose() {
// Remove the primary logger and register a noop logger.
log.Remove(log.DefaultConsoleName)
err := log.New("noop", testutil.InitNoopLogger)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
os.Exit(m.Run())
}

View File

@ -13,6 +13,7 @@ import (
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/authutil"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/lfsutil"
)
@ -26,10 +27,17 @@ func RegisterRoutes(r *macaron.Router) {
r.Group("", func() {
r.Post("/objects/batch", authorize(db.AccessModeRead), verifyAccept, verifyContentTypeJSON, serveBatch)
r.Group("/objects/basic", func() {
basic := &basicHandler{
defaultStorage: lfsutil.Storage(conf.LFS.Storage),
storagers: map[lfsutil.Storage]lfsutil.Storager{
lfsutil.StorageLocal: &lfsutil.LocalStorage{Root: conf.LFS.ObjectsPath},
},
}
r.Combo("/:oid", verifyOID()).
Get(authorize(db.AccessModeRead), serveBasicDownload).
Put(authorize(db.AccessModeWrite), verifyContentTypeStream, serveBasicUpload)
r.Post("/verify", authorize(db.AccessModeWrite), verifyAccept, verifyContentTypeJSON, serveBasicVerify)
Get(authorize(db.AccessModeRead), basic.serveDownload).
Put(authorize(db.AccessModeWrite), verifyContentTypeStream, basic.serveUpload)
r.Post("/verify", authorize(db.AccessModeWrite), verifyAccept, verifyContentTypeJSON, basic.serveVerify)
})
}, authenticate())
}

View File

@ -0,0 +1,31 @@
// Copyright 2020 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package testutil
import (
log "unknwon.dev/clog/v2"
)
var _ log.Logger = (*noopLogger)(nil)
// noopLogger is a placeholder logger that logs nothing.
type noopLogger struct{}
func (l *noopLogger) Name() string {
return "noop"
}
func (l *noopLogger) Level() log.Level {
return log.LevelTrace
}
func (l *noopLogger) Write(log.Messager) error {
return nil
}
// InitNoopLogger is a init function to initialize a noop logger.
var InitNoopLogger = func(name string, vs ...interface{}) (log.Logger, error) {
return &noopLogger{}, nil
}