diff --git a/locking/locks.go b/locking/locks.go index 9c63f560..49f06579 100644 --- a/locking/locks.go +++ b/locking/locks.go @@ -1,6 +1,7 @@ package locking import ( + "encoding/json" "fmt" "net/http" "os" @@ -41,6 +42,7 @@ type Client struct { RemoteRef *git.Ref client *lockClient cache LockCacher + cacheDir string lockablePatterns []string lockableFilter *filepathfilter.Filter @@ -79,6 +81,7 @@ func (c *Client) SetupFileCache(path string) error { } c.cache = cache + c.cacheDir = filepath.Join(path, "cache") return nil } @@ -206,7 +209,21 @@ func (c *Client) SearchLocks(filter map[string]string, limit int, localOnly bool if localOnly { return c.searchLocalLocks(filter, limit) } else { - return c.searchRemoteLocks(filter, limit) + locks, err := c.searchRemoteLocks(filter, limit) + if err != nil { + return locks, err + } + + if len(filter) == 0 && limit == 0 { + cacheFile, err := c.prepareCacheDirectory() + if err != nil { + return locks, err + } + + err = c.writeLocksToCacheFile(cacheFile, locks) + } + + return locks, err } } @@ -384,6 +401,44 @@ func init() { kv.RegisterTypeForStorage(&Lock{}) } +func (c *Client) prepareCacheDirectory() (string, error) { + cacheDir := filepath.Join(c.cacheDir, "locks") + if c.RemoteRef != nil { + cacheDir = filepath.Join(cacheDir, c.RemoteRef.Refspec()) + } + + stat, err := os.Stat(cacheDir) + if err == nil { + if !stat.IsDir() { + return cacheDir, errors.New("init cache directory " + cacheDir + " failed: already exists, but is no directory") + } + } else if os.IsNotExist(err) { + err = os.MkdirAll(cacheDir, os.ModePerm) + if err != nil { + return cacheDir, errors.Wrap(err, "init cache directory "+cacheDir+" failed: directory creation failed") + } + } else { + return cacheDir, errors.Wrap(err, "init cache directory "+cacheDir+" failed") + } + + return filepath.Join(cacheDir, "remote"), nil +} + +func (c *Client) writeLocksToCacheFile(path string, locks []Lock) error { + file, err := os.Create(path) + if err != nil { + return err + } + + err = json.NewEncoder(file).Encode(locks) + if err != nil { + file.Close() + return err + } + + return file.Close() +} + type nilLockCacher struct{} func (c *nilLockCacher) Add(l Lock) error { diff --git a/locking/locks_test.go b/locking/locks_test.go index 128afffe..2bc9d199 100644 --- a/locking/locks_test.go +++ b/locking/locks_test.go @@ -5,10 +5,12 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" "sort" "testing" "time" + "github.com/git-lfs/git-lfs/git" "github.com/git-lfs/git-lfs/lfsapi" "github.com/git-lfs/git-lfs/lfshttp" "github.com/stretchr/testify/assert" @@ -21,6 +23,89 @@ func (a LocksById) Len() int { return len(a) } func (a LocksById) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a LocksById) Less(i, j int) bool { return a[i].Id < a[j].Id } +func TestRemoteLocksWithCache(t *testing.T) { + var err error + tempDir, err := ioutil.TempDir("", "testCacheLock") + assert.Nil(t, err) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/api/locks", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(&lockList{ + Locks: []Lock{ + Lock{Id: "100", Path: "folder/test1.dat", Owner: &User{Name: "Alice"}}, + Lock{Id: "101", Path: "folder/test2.dat", Owner: &User{Name: "Charles"}}, + Lock{Id: "102", Path: "folder/test3.dat", Owner: &User{Name: "Fred"}}, + }, + }) + assert.Nil(t, err) + })) + + defer func() { + srv.Close() + }() + + lfsclient, err := lfsapi.NewClient(lfshttp.NewContext(nil, nil, map[string]string{ + "lfs.url": srv.URL + "/api", + "user.name": "Fred", + "user.email": "fred@bloggs.com", + })) + require.Nil(t, err) + + client, err := NewClient("", lfsclient) + assert.Nil(t, err) + assert.Nil(t, client.SetupFileCache(tempDir)) + + client.RemoteRef = &git.Ref{Name: "refs/heads/master"} + cacheFile, err := client.prepareCacheDirectory() + assert.Nil(t, err) + + // Cache file should not exist + fi, err := os.Stat(cacheFile) + assert.True(t, os.IsNotExist(err)) + + // Need to include zero time in structure for equal to work + zeroTime := time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC) + + // REMOTE QUERY: No cache file will be created when querying with a filter + locks, err := client.SearchLocks(map[string]string{ + "key": "value", + }, 0, false) + assert.Nil(t, err) + // Just make sure we have have received anything, content doesn't matter + assert.Equal(t, 3, len(locks)) + + fi, err = os.Stat(cacheFile) + assert.True(t, os.IsNotExist(err)) + + // REMOTE QUERY: No cache file will be created when querying with a limit + locks, err = client.SearchLocks(nil, 1, false) + assert.Nil(t, err) + // Just make sure we have have received anything, content doesn't matter + assert.Equal(t, 1, len(locks)) + + fi, err = os.Stat(cacheFile) + assert.True(t, os.IsNotExist(err)) + + // REMOTE QUERY: locks will be reported and cache file should be created + locks, err = client.SearchLocks(nil, 0, false) + assert.Nil(t, err) + + fi, err = os.Stat(cacheFile) + assert.Nil(t, err) + const size int64 = 300 + assert.Equal(t, size, fi.Size()) + + sort.Sort(LocksById(locks)) + assert.Equal(t, []Lock{ + Lock{Path: "folder/test1.dat", Id: "100", Owner: &User{Name: "Alice"}, LockedAt: zeroTime}, + Lock{Path: "folder/test2.dat", Id: "101", Owner: &User{Name: "Charles"}, LockedAt: zeroTime}, + Lock{Path: "folder/test3.dat", Id: "102", Owner: &User{Name: "Fred"}, LockedAt: zeroTime}, + }, locks) +} + func TestRefreshCache(t *testing.T) { var err error tempDir, err := ioutil.TempDir("", "testCacheLock")