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 }