diff options
author | ᴜɴᴋɴᴡᴏɴ <u@gogs.io> | 2020-04-10 22:13:42 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-10 22:13:42 +0800 |
commit | 9a5b227f3ee2b2a3854d3aec022cc9a0cf0868b3 (patch) | |
tree | 7600826f4affce28e95588de921d801f0414f046 /internal/lfsutil | |
parent | 3e055e329cf93eb5de77562d7795240808d31c08 (diff) |
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
Diffstat (limited to 'internal/lfsutil')
-rw-r--r-- | internal/lfsutil/oid.go | 4 | ||||
-rw-r--r-- | internal/lfsutil/storage.go | 93 | ||||
-rw-r--r-- | internal/lfsutil/storage_test.go | 105 |
3 files changed, 190 insertions, 12 deletions
diff --git a/internal/lfsutil/oid.go b/internal/lfsutil/oid.go index 16d0ad3d..ad19bd5b 100644 --- a/internal/lfsutil/oid.go +++ b/internal/lfsutil/oid.go @@ -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)) diff --git a/internal/lfsutil/storage.go b/internal/lfsutil/storage.go index b2bfe37f..a357b8a8 100644 --- a/internal/lfsutil/storage.go +++ b/internal/lfsutil/storage.go @@ -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 } diff --git a/internal/lfsutil/storage_test.go b/internal/lfsutil/storage_test.go index bfb69e4a..86702758 100644 --- a/internal/lfsutil/storage_test.go +++ b/internal/lfsutil/storage_test.go @@ -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) }) } } |