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.
This commit is contained in:
Marc Strapetz 2018-10-07 12:36:15 +02:00
parent d7cf4e18f1
commit e811a46ce7
2 changed files with 141 additions and 1 deletions

@ -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 {

@ -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")