mirror of https://github.com/gogs/gogs.git
lfsutil: add `Storager` interface and local storage (#6083)
* Add Storager interface * Add tests * Add back note * Add tests for basic protocol routes * Fix lint errorspull/6084/head
parent
3e055e329c
commit
9a5b227f3e
|
@ -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
|
@ -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:","`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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"})
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue