aboutsummaryrefslogtreecommitdiff
path: root/internal/lfsutil
diff options
context:
space:
mode:
authorᴜɴᴋɴᴡᴏɴ <u@gogs.io>2020-04-10 22:13:42 +0800
committerGitHub <noreply@github.com>2020-04-10 22:13:42 +0800
commit9a5b227f3ee2b2a3854d3aec022cc9a0cf0868b3 (patch)
tree7600826f4affce28e95588de921d801f0414f046 /internal/lfsutil
parent3e055e329cf93eb5de77562d7795240808d31c08 (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.go4
-rw-r--r--internal/lfsutil/storage.go93
-rw-r--r--internal/lfsutil/storage_test.go105
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)
})
}
}