1package main
2
3import (
4	"encoding/gob"
5	"errors"
6	"os"
7	"path/filepath"
8	"sync"
9	"time"
10)
11
12var defaultLockFile = filepath.Join(userCacheDir, "o/lockfile.txt")
13
14// LockKeeper keeps track of which files are currently being edited by o
15type LockKeeper struct {
16	lockedFiles  map[string]time.Time // from filename to lockfilestamp
17	mut          *sync.RWMutex
18	lockFilename string
19}
20
21// NewLockKeeper takes an expanded path (not containing ~) to a lock file
22// and creates a new LockKeeper struct, without loading the given lock file.
23func NewLockKeeper(lockFilename string) *LockKeeper {
24	lockMap := make(map[string]time.Time)
25	return &LockKeeper{lockMap, &sync.RWMutex{}, lockFilename}
26}
27
28// Load loads the contents of the main lockfile
29func (lk *LockKeeper) Load() error {
30	f, err := os.Open(lk.lockFilename)
31	if err != nil {
32		return err
33	}
34	defer f.Close()
35
36	f.Sync()
37
38	dec := gob.NewDecoder(f)
39	lockMap := make(map[string]time.Time)
40	err = dec.Decode(&lockMap)
41	if err != nil {
42		return err
43	}
44	lk.mut.Lock()
45	lk.lockedFiles = lockMap
46	lk.mut.Unlock()
47	return nil
48}
49
50// Save writes the contents of the main lockfile
51func (lk *LockKeeper) Save() error {
52	// First create the folder for the lock file overview, if needed
53	folderPath := filepath.Dir(lk.lockFilename)
54	os.MkdirAll(folderPath, os.ModePerm)
55
56	f, err := os.OpenFile(lk.lockFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
57	if err != nil {
58		return err
59	}
60	defer f.Close()
61
62	enc := gob.NewEncoder(f)
63
64	lk.mut.RLock()
65	err = enc.Encode(lk.lockedFiles)
66	lk.mut.RUnlock()
67
68	f.Sync()
69
70	return err
71}
72
73// Lock marks the given absolute filename as locked.
74// If the file is already locked, an error is returned.
75func (lk *LockKeeper) Lock(filename string) error {
76	var has bool
77	lk.mut.RLock()
78	_, has = lk.lockedFiles[filename]
79	lk.mut.RUnlock()
80
81	if has {
82		return errors.New("already locked: " + filename)
83	}
84
85	// Add the file to the map
86	lk.mut.Lock()
87	lk.lockedFiles[filename] = time.Now()
88	lk.mut.Unlock()
89
90	return nil
91}
92
93// Unlock marks the given absolute filename as unlocked.
94// If the file is already unlocked, an error is returned.
95func (lk *LockKeeper) Unlock(filename string) error {
96	var has bool
97	lk.mut.RLock()
98	_, has = lk.lockedFiles[filename]
99	lk.mut.RUnlock()
100
101	if !has {
102		// Caller can ignore this error if they want
103		return errors.New("already unlocked: " + filename)
104	}
105
106	// Remove the file from the map
107	lk.mut.Lock()
108	delete(lk.lockedFiles, filename)
109	lk.mut.Unlock()
110
111	return nil
112}
113
114// GetTimestamp assumes that the file is locked. A blank timestamp may be returned if not.
115func (lk *LockKeeper) GetTimestamp(filename string) time.Time {
116	var timestamp time.Time
117
118	lk.mut.RLock()
119	timestamp = lk.lockedFiles[filename]
120	lk.mut.RUnlock()
121
122	return timestamp
123}
124