From e811a46ce7a7acf52752868dcabcba912ea785a3 Mon Sep 17 00:00:00 2001 From: Marc Strapetz Date: Sun, 7 Oct 2018 12:36:15 +0200 Subject: [PATCH] locking: write JSON response for "list locks" API call to a cache file Results of calling "list locks" API will be written in human-readable format to .git/lfs/cache/locks. Cache files will only be written for queries which are not limited or filtered. There will be a separate cache file for every remote ref (Client.RemoteRef). Cache file format is JSON, which should be considered as implementation detail and there is no guarantee that the file format will not be changed for future Git-LFS releases. The cache file can later be read by Git-LFS or third party tools which wish to retrieve the last known lock information without invoking "git lfs locks --cached" (e.g. for performance reasons). This commit prepares for a fix of issue #3107. --- locking/locks.go | 57 ++++++++++++++++++++++++++++- locking/locks_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) 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")