github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+incompatible/locking/locks.go (about)

     1  package locking
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"os"
     7  	"path/filepath"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/git-lfs/git-lfs/errors"
    12  	"github.com/git-lfs/git-lfs/filepathfilter"
    13  	"github.com/git-lfs/git-lfs/lfsapi"
    14  	"github.com/git-lfs/git-lfs/tools"
    15  	"github.com/git-lfs/git-lfs/tools/kv"
    16  	"github.com/rubyist/tracerx"
    17  )
    18  
    19  var (
    20  	// ErrNoMatchingLocks is an error returned when no matching locks were
    21  	// able to be resolved
    22  	ErrNoMatchingLocks = errors.New("lfs: no matching locks found")
    23  	// ErrLockAmbiguous is an error returned when multiple matching locks
    24  	// were found
    25  	ErrLockAmbiguous = errors.New("lfs: multiple locks found; ambiguous")
    26  )
    27  
    28  type LockCacher interface {
    29  	Add(l Lock) error
    30  	RemoveByPath(filePath string) error
    31  	RemoveById(id string) error
    32  	Locks() []Lock
    33  	Clear()
    34  	Save() error
    35  }
    36  
    37  // Client is the main interface object for the locking package
    38  type Client struct {
    39  	Remote string
    40  	client *lockClient
    41  	cache  LockCacher
    42  
    43  	lockablePatterns []string
    44  	lockableFilter   *filepathfilter.Filter
    45  	lockableMutex    sync.Mutex
    46  
    47  	LocalWorkingDir          string
    48  	LocalGitDir              string
    49  	SetLockableFilesReadOnly bool
    50  }
    51  
    52  // NewClient creates a new locking client with the given configuration
    53  // You must call the returned object's `Close` method when you are finished with
    54  // it
    55  func NewClient(remote string, lfsClient *lfsapi.Client) (*Client, error) {
    56  	return &Client{
    57  		Remote: remote,
    58  		client: &lockClient{Client: lfsClient},
    59  		cache:  &nilLockCacher{},
    60  	}, nil
    61  }
    62  
    63  func (c *Client) SetupFileCache(path string) error {
    64  	stat, err := os.Stat(path)
    65  	if err != nil {
    66  		return errors.Wrap(err, "init lock cache")
    67  	}
    68  
    69  	lockFile := path
    70  	if stat.IsDir() {
    71  		lockFile = filepath.Join(path, "lockcache.db")
    72  	}
    73  
    74  	cache, err := NewLockCache(lockFile)
    75  	if err != nil {
    76  		return errors.Wrap(err, "init lock cache")
    77  	}
    78  
    79  	c.cache = cache
    80  	return nil
    81  }
    82  
    83  // Close this client instance; must be called to dispose of resources
    84  func (c *Client) Close() error {
    85  	return c.cache.Save()
    86  }
    87  
    88  // LockFile attempts to lock a file on the current remote
    89  // path must be relative to the root of the repository
    90  // Returns the lock id if successful, or an error
    91  func (c *Client) LockFile(path string) (Lock, error) {
    92  	lockRes, _, err := c.client.Lock(c.Remote, &lockRequest{Path: path})
    93  	if err != nil {
    94  		return Lock{}, errors.Wrap(err, "api")
    95  	}
    96  
    97  	if len(lockRes.Message) > 0 {
    98  		if len(lockRes.RequestID) > 0 {
    99  			tracerx.Printf("Server Request ID: %s", lockRes.RequestID)
   100  		}
   101  		return Lock{}, fmt.Errorf("Server unable to create lock: %s", lockRes.Message)
   102  	}
   103  
   104  	lock := *lockRes.Lock
   105  	if err := c.cache.Add(lock); err != nil {
   106  		return Lock{}, errors.Wrap(err, "lock cache")
   107  	}
   108  
   109  	// Ensure writeable on return
   110  	if err := tools.SetFileWriteFlag(path, true); err != nil {
   111  		return Lock{}, err
   112  	}
   113  
   114  	return lock, nil
   115  }
   116  
   117  // UnlockFile attempts to unlock a file on the current remote
   118  // path must be relative to the root of the repository
   119  // Force causes the file to be unlocked from other users as well
   120  func (c *Client) UnlockFile(path string, force bool) error {
   121  	id, err := c.lockIdFromPath(path)
   122  	if err != nil {
   123  		return fmt.Errorf("Unable to get lock id: %v", err)
   124  	}
   125  
   126  	err = c.UnlockFileById(id, force)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	// Make non-writeable if required
   132  	if c.SetLockableFilesReadOnly && c.IsFileLockable(path) {
   133  		return tools.SetFileWriteFlag(path, false)
   134  	}
   135  	return nil
   136  
   137  }
   138  
   139  // UnlockFileById attempts to unlock a lock with a given id on the current remote
   140  // Force causes the file to be unlocked from other users as well
   141  func (c *Client) UnlockFileById(id string, force bool) error {
   142  	unlockRes, _, err := c.client.Unlock(c.Remote, id, force)
   143  	if err != nil {
   144  		return errors.Wrap(err, "api")
   145  	}
   146  
   147  	if len(unlockRes.Message) > 0 {
   148  		if len(unlockRes.RequestID) > 0 {
   149  			tracerx.Printf("Server Request ID: %s", unlockRes.RequestID)
   150  		}
   151  		return fmt.Errorf("Server unable to unlock: %s", unlockRes.Message)
   152  	}
   153  
   154  	if err := c.cache.RemoveById(id); err != nil {
   155  		return fmt.Errorf("Error caching unlock information: %v", err)
   156  	}
   157  
   158  	return nil
   159  }
   160  
   161  // Lock is a record of a locked file
   162  type Lock struct {
   163  	// Id is the unique identifier corresponding to this particular Lock. It
   164  	// must be consistent with the local copy, and the server's copy.
   165  	Id string `json:"id"`
   166  	// Path is an absolute path to the file that is locked as a part of this
   167  	// lock.
   168  	Path string `json:"path"`
   169  	// Owner is the identity of the user that created this lock.
   170  	Owner *User `json:"owner,omitempty"`
   171  	// LockedAt is the time at which this lock was acquired.
   172  	LockedAt time.Time `json:"locked_at"`
   173  }
   174  
   175  // SearchLocks returns a channel of locks which match the given name/value filter
   176  // If limit > 0 then search stops at that number of locks
   177  // If localOnly = true, don't query the server & report only own local locks
   178  func (c *Client) SearchLocks(filter map[string]string, limit int, localOnly bool) ([]Lock, error) {
   179  	if localOnly {
   180  		return c.searchCachedLocks(filter, limit)
   181  	} else {
   182  		return c.searchRemoteLocks(filter, limit)
   183  	}
   184  }
   185  
   186  func (c *Client) VerifiableLocks(limit int) (ourLocks, theirLocks []Lock, err error) {
   187  	ourLocks = make([]Lock, 0, limit)
   188  	theirLocks = make([]Lock, 0, limit)
   189  	body := &lockVerifiableRequest{
   190  		Limit: limit,
   191  	}
   192  
   193  	for {
   194  		list, res, err := c.client.SearchVerifiable(c.Remote, body)
   195  		if res != nil {
   196  			switch res.StatusCode {
   197  			case http.StatusNotFound, http.StatusNotImplemented:
   198  				return ourLocks, theirLocks, errors.NewNotImplementedError(err)
   199  			case http.StatusForbidden:
   200  				return ourLocks, theirLocks, errors.NewAuthError(err)
   201  			}
   202  		}
   203  
   204  		if err != nil {
   205  			return ourLocks, theirLocks, err
   206  		}
   207  
   208  		if list.Message != "" {
   209  			if len(list.RequestID) > 0 {
   210  				tracerx.Printf("Server Request ID: %s", list.RequestID)
   211  			}
   212  			return ourLocks, theirLocks, fmt.Errorf("Server error searching locks: %s", list.Message)
   213  		}
   214  
   215  		for _, l := range list.Ours {
   216  			ourLocks = append(ourLocks, l)
   217  			if limit > 0 && (len(ourLocks)+len(theirLocks)) >= limit {
   218  				return ourLocks, theirLocks, nil
   219  			}
   220  		}
   221  
   222  		for _, l := range list.Theirs {
   223  			theirLocks = append(theirLocks, l)
   224  			if limit > 0 && (len(ourLocks)+len(theirLocks)) >= limit {
   225  				return ourLocks, theirLocks, nil
   226  			}
   227  		}
   228  
   229  		if list.NextCursor != "" {
   230  			body.Cursor = list.NextCursor
   231  		} else {
   232  			break
   233  		}
   234  	}
   235  
   236  	return ourLocks, theirLocks, nil
   237  }
   238  
   239  func (c *Client) searchCachedLocks(filter map[string]string, limit int) ([]Lock, error) {
   240  	cachedlocks := c.cache.Locks()
   241  	path, filterByPath := filter["path"]
   242  	id, filterById := filter["id"]
   243  	lockCount := 0
   244  	locks := make([]Lock, 0, len(cachedlocks))
   245  	for _, l := range cachedlocks {
   246  		// Manually filter by Path/Id
   247  		if (filterByPath && path != l.Path) ||
   248  			(filterById && id != l.Id) {
   249  			continue
   250  		}
   251  		locks = append(locks, l)
   252  		lockCount++
   253  		if limit > 0 && lockCount >= limit {
   254  			break
   255  		}
   256  	}
   257  	return locks, nil
   258  }
   259  
   260  func (c *Client) searchRemoteLocks(filter map[string]string, limit int) ([]Lock, error) {
   261  	locks := make([]Lock, 0, limit)
   262  
   263  	apifilters := make([]lockFilter, 0, len(filter))
   264  	for k, v := range filter {
   265  		apifilters = append(apifilters, lockFilter{Property: k, Value: v})
   266  	}
   267  	query := &lockSearchRequest{Filters: apifilters, Limit: limit}
   268  	for {
   269  		list, _, err := c.client.Search(c.Remote, query)
   270  		if err != nil {
   271  			return locks, errors.Wrap(err, "locking")
   272  		}
   273  
   274  		if list.Message != "" {
   275  			if len(list.RequestID) > 0 {
   276  				tracerx.Printf("Server Request ID: %s", list.RequestID)
   277  			}
   278  			return locks, fmt.Errorf("Server error searching for locks: %s", list.Message)
   279  		}
   280  
   281  		for _, l := range list.Locks {
   282  			locks = append(locks, l)
   283  			if limit > 0 && len(locks) >= limit {
   284  				// Exit outer loop too
   285  				return locks, nil
   286  			}
   287  		}
   288  
   289  		if list.NextCursor != "" {
   290  			query.Cursor = list.NextCursor
   291  		} else {
   292  			break
   293  		}
   294  	}
   295  
   296  	return locks, nil
   297  }
   298  
   299  // lockIdFromPath makes a call to the LFS API and resolves the ID for the locked
   300  // locked at the given path.
   301  //
   302  // If the API call failed, an error will be returned. If multiple locks matched
   303  // the given path (should not happen during real-world usage), an error will be
   304  // returnd. If no locks matched the given path, an error will be returned.
   305  //
   306  // If the API call is successful, and only one lock matches the given filepath,
   307  // then its ID will be returned, along with a value of "nil" for the error.
   308  func (c *Client) lockIdFromPath(path string) (string, error) {
   309  	list, _, err := c.client.Search(c.Remote, &lockSearchRequest{
   310  		Filters: []lockFilter{
   311  			{Property: "path", Value: path},
   312  		},
   313  	})
   314  
   315  	if err != nil {
   316  		return "", err
   317  	}
   318  
   319  	switch len(list.Locks) {
   320  	case 0:
   321  		return "", ErrNoMatchingLocks
   322  	case 1:
   323  		return list.Locks[0].Id, nil
   324  	default:
   325  		return "", ErrLockAmbiguous
   326  	}
   327  }
   328  
   329  // Fetch locked files for the current user and cache them locally
   330  // This can be used to sync up locked files when moving machines
   331  func (c *Client) refreshLockCache() error {
   332  	ourLocks, _, err := c.VerifiableLocks(0)
   333  	if err != nil {
   334  		return err
   335  	}
   336  
   337  	// We're going to overwrite the entire local cache
   338  	c.cache.Clear()
   339  	for _, l := range ourLocks {
   340  		c.cache.Add(l)
   341  	}
   342  
   343  	return nil
   344  }
   345  
   346  // IsFileLockedByCurrentCommitter returns whether a file is locked by the
   347  // current user, as cached locally
   348  func (c *Client) IsFileLockedByCurrentCommitter(path string) bool {
   349  	filter := map[string]string{"path": path}
   350  	locks, err := c.searchCachedLocks(filter, 1)
   351  	if err != nil {
   352  		tracerx.Printf("Error searching cached locks: %s\nForcing remote search", err)
   353  		locks, _ = c.searchRemoteLocks(filter, 1)
   354  	}
   355  	return len(locks) > 0
   356  }
   357  
   358  func init() {
   359  	kv.RegisterTypeForStorage(&Lock{})
   360  }
   361  
   362  type nilLockCacher struct{}
   363  
   364  func (c *nilLockCacher) Add(l Lock) error {
   365  	return nil
   366  }
   367  func (c *nilLockCacher) RemoveByPath(filePath string) error {
   368  	return nil
   369  }
   370  func (c *nilLockCacher) RemoveById(id string) error {
   371  	return nil
   372  }
   373  func (c *nilLockCacher) Locks() []Lock {
   374  	return nil
   375  }
   376  func (c *nilLockCacher) Clear() {}
   377  func (c *nilLockCacher) Save() error {
   378  	return nil
   379  }