1package libkbfs
2
3import (
4	"os"
5	"path"
6	"strconv"
7	"sync"
8
9	"github.com/keybase/client/go/kbfs/idutil"
10	"github.com/keybase/client/go/kbfs/ldbutils"
11	"github.com/keybase/client/go/libkb"
12	"github.com/keybase/client/go/logger"
13	"github.com/keybase/client/go/protocol/keybase1"
14	"github.com/pkg/errors"
15	"github.com/syndtr/goleveldb/leveldb"
16	"github.com/syndtr/goleveldb/leveldb/opt"
17	"github.com/syndtr/goleveldb/leveldb/storage"
18	"golang.org/x/net/context"
19)
20
21const (
22	// Where in config.StorageRoot() we store settings information.
23	settingsDBDir           = "kbfs_settings"
24	settingsDBVersionString = "v1"
25	settingsDBName          = "kbfsSettings.leveldb"
26
27	// Settings keys
28	spaceAvailableNotificationThresholdKey = "spaceAvailableNotificationThreshold"
29
30	sfmiBannerDismissedKey = "sfmiBannerDismissed"
31	syncOnCellularKey      = "syncOnCellular"
32)
33
34// ErrNoSettingsDB is returned when there is no settings DB potentially due to
35// multiple concurrent KBFS instances.
36var ErrNoSettingsDB = errors.New("no settings DB")
37
38var errNoSession = errors.New("no session")
39
40type currentSessionGetter interface {
41	CurrentSessionGetter() idutil.CurrentSessionGetter
42}
43
44// SettingsDB stores KBFS user settings for a given device.
45type SettingsDB struct {
46	*ldbutils.LevelDb
47	sessionGetter currentSessionGetter
48	logger        logger.Logger
49	vlogger       *libkb.VDebugLog
50
51	lock  sync.RWMutex
52	cache map[string][]byte
53}
54
55func openSettingsDBInternal(config Config) (*ldbutils.LevelDb, error) {
56	if config.IsTestMode() {
57		return ldbutils.OpenLevelDb(storage.NewMemStorage(), config.Mode())
58	}
59	dbPath := path.Join(config.StorageRoot(), settingsDBDir,
60		settingsDBVersionString)
61	err := os.MkdirAll(dbPath, os.ModePerm)
62	if err != nil {
63		return nil, err
64	}
65
66	stor, err := storage.OpenFile(path.Join(dbPath, settingsDBName), false)
67	if err != nil {
68		return nil, err
69	}
70
71	return ldbutils.OpenLevelDb(stor, config.Mode())
72}
73
74func openSettingsDB(config Config) *SettingsDB {
75	logger := config.MakeLogger("SDB")
76	vlogger := config.MakeVLogger(logger)
77	db, err := openSettingsDBInternal(config)
78	if err != nil {
79		logger.CWarningf(context.Background(),
80			"Could not open settings DB. "+
81				"Perhaps multiple KBFS instances are being run concurrently"+
82				"? Error: %+v", err)
83		if db != nil {
84			db.Close()
85		}
86		return nil
87	}
88	return &SettingsDB{
89		LevelDb:       db,
90		sessionGetter: config,
91		logger:        logger,
92		vlogger:       vlogger,
93		cache:         make(map[string][]byte),
94	}
95}
96
97func (db *SettingsDB) getUID(ctx context.Context) keybase1.UID {
98	if db.sessionGetter == nil || db.sessionGetter.CurrentSessionGetter() == nil {
99		return keybase1.UID("")
100	}
101	si, err := db.sessionGetter.CurrentSessionGetter().GetCurrentSession(ctx)
102	if err != nil {
103		return keybase1.UID("")
104	}
105	return si.UID
106}
107
108func getSettingsDbKey(uid keybase1.UID, key string) []byte {
109	return append([]byte(uid), []byte(key)...)
110}
111
112func (db *SettingsDB) getFromCache(key string) (val []byte, isCached bool) {
113	db.lock.RLock()
114	defer db.lock.RUnlock()
115	val, isCached = db.cache[key]
116	return val, isCached
117}
118
119func (db *SettingsDB) updateCache(key string, val []byte) {
120	db.lock.Lock()
121	defer db.lock.Unlock()
122	if val == nil {
123		delete(db.cache, key)
124	} else {
125		db.cache[key] = val
126	}
127}
128
129// Get overrides (*LevelDb).Get to cache values in memory.
130func (db *SettingsDB) Get(key []byte, ro *opt.ReadOptions) ([]byte, error) {
131	val, isCached := db.getFromCache(string(key))
132	if isCached {
133		return val, nil
134	}
135	val, err := db.LevelDb.Get(key, ro)
136	if err == nil {
137		db.updateCache(string(key), val)
138	}
139	return val, err
140}
141
142// Put overrides (*LevelDb).Put to cache values in memory.
143func (db *SettingsDB) Put(key []byte, val []byte, wo *opt.WriteOptions) error {
144	err := db.LevelDb.Put(key, val, wo)
145	if err != nil {
146		db.updateCache(string(key), nil)
147	} else {
148		db.updateCache(string(key), val)
149	}
150	return err
151}
152
153// Settings returns the logged-in user's settings as a keybase1.FSSettings.
154func (db *SettingsDB) Settings(ctx context.Context) (keybase1.FSSettings, error) {
155	uid := db.getUID(ctx)
156	if uid == keybase1.UID("") {
157		return keybase1.FSSettings{}, errNoSession
158	}
159
160	var notificationThreshold int64
161	notificationThresholdBytes, err :=
162		db.Get(getSettingsDbKey(uid, spaceAvailableNotificationThresholdKey), nil)
163	switch errors.Cause(err) {
164	case leveldb.ErrNotFound:
165		db.vlogger.CLogf(ctx, libkb.VLog1,
166			"notificationThreshold not set; using default value")
167	case nil:
168		notificationThreshold, err =
169			strconv.ParseInt(string(notificationThresholdBytes), 10, 64)
170		if err != nil {
171			return keybase1.FSSettings{}, err
172		}
173	default:
174		db.logger.CWarningf(ctx,
175			"reading notificationThreshold from leveldb error: %+v", err)
176		return keybase1.FSSettings{}, err
177	}
178
179	var sfmiBannerDismissed bool
180	sfmiBannerDismissedBytes, err :=
181		db.Get(getSettingsDbKey(uid, sfmiBannerDismissedKey), nil)
182	switch errors.Cause(err) {
183	case leveldb.ErrNotFound:
184		db.vlogger.CLogf(ctx, libkb.VLog1,
185			"sfmiBannerDismissed not set; using default value")
186	case nil:
187		sfmiBannerDismissed, err =
188			strconv.ParseBool(string(sfmiBannerDismissedBytes))
189		if err != nil {
190			return keybase1.FSSettings{}, err
191		}
192	default:
193		db.logger.CWarningf(ctx,
194			"reading sfmiBannerDismissed from leveldb error: %+v", err)
195		return keybase1.FSSettings{}, err
196	}
197
198	var syncOnCellular bool
199	syncOnCellularBytes, err :=
200		db.Get(getSettingsDbKey(uid, syncOnCellularKey), nil)
201	switch errors.Cause(err) {
202	case leveldb.ErrNotFound:
203		db.vlogger.CLogf(ctx, libkb.VLog1,
204			"syncOnCellular not set; using default value")
205	case nil:
206		syncOnCellular, err = strconv.ParseBool(string(syncOnCellularBytes))
207		if err != nil {
208			return keybase1.FSSettings{}, err
209		}
210	default:
211		db.logger.CWarningf(ctx,
212			"reading syncOnCellular from leveldb error: %+v", err)
213		return keybase1.FSSettings{}, err
214	}
215
216	return keybase1.FSSettings{
217		SpaceAvailableNotificationThreshold: notificationThreshold,
218		SfmiBannerDismissed:                 sfmiBannerDismissed,
219		SyncOnCellular:                      syncOnCellular,
220	}, nil
221}
222
223// SetNotificationThreshold sets the notification threshold setting for the
224// logged-in user.
225func (db *SettingsDB) SetNotificationThreshold(
226	ctx context.Context, threshold int64) error {
227	uid := db.getUID(ctx)
228	if uid == keybase1.UID("") {
229		return errNoSession
230	}
231	return db.Put(getSettingsDbKey(uid, spaceAvailableNotificationThresholdKey),
232		[]byte(strconv.FormatInt(threshold, 10)), nil)
233}
234
235// SetSfmiBannerDismissed sets whether the smfi banner has been dismissed.
236func (db *SettingsDB) SetSfmiBannerDismissed(
237	ctx context.Context, dismissed bool) error {
238	uid := db.getUID(ctx)
239	if uid == keybase1.UID("") {
240		return errNoSession
241	}
242	return db.Put(getSettingsDbKey(uid, sfmiBannerDismissedKey),
243		[]byte(strconv.FormatBool(dismissed)), nil)
244}
245
246// SetSyncOnCellular sets whether we should do TLF syncing on a
247// cellular network.
248func (db *SettingsDB) SetSyncOnCellular(
249	ctx context.Context, syncOnCellular bool) error {
250	uid := db.getUID(ctx)
251	if uid == keybase1.UID("") {
252		return errNoSession
253	}
254	return db.Put(getSettingsDbKey(uid, syncOnCellularKey),
255		[]byte(strconv.FormatBool(syncOnCellular)), nil)
256}
257