1// Copyright 2016 Keybase Inc. All rights reserved.
2// Use of this source code is governed by a BSD
3// license that can be found in the LICENSE file.
4
5package libkbfs
6
7import (
8	"errors"
9	"fmt"
10	"os"
11	"strings"
12	"sync"
13	"time"
14
15	"github.com/keybase/client/go/kbfs/data"
16	"github.com/keybase/client/go/kbfs/favorites"
17	"github.com/keybase/client/go/kbfs/kbfssync"
18	"github.com/keybase/client/go/kbfs/ldbutils"
19	"github.com/keybase/client/go/kbfs/tlf"
20	"github.com/keybase/client/go/libkb"
21	"github.com/keybase/client/go/logger"
22	"github.com/keybase/client/go/protocol/keybase1"
23	"github.com/syndtr/goleveldb/leveldb"
24	"github.com/syndtr/goleveldb/leveldb/storage"
25	"golang.org/x/net/context"
26)
27
28const (
29	disableFavoritesEnvVar           = "KEYBASE_DISABLE_FAVORITES"
30	favoritesCacheExpirationTime     = time.Hour * 24 * 7 // one week
31	kbfsFavoritesCacheSubfolder      = "kbfs_favorites"
32	favoritesDiskCacheFilename       = "kbfsFavorites.leveldb"
33	favoritesDiskCacheVersion        = 2
34	favoritesDiskCacheStorageVersion = 1
35	// How long to block on favorites refresh when cache is expired (e.g.,
36	// on startup). Reasonably low in case we're offline.
37	favoritesServerTimeoutWhenCacheExpired = 500 * time.Millisecond
38	favoritesBackgroundRefreshTimeout      = 15 * time.Second
39	defaultFavoritesBufferedReqInterval    = 5 * time.Second
40)
41
42var errNoFavoritesCache = errors.New("disk favorites cache not present")
43
44type errIncorrectFavoritesCacheVersion struct {
45	cache   string
46	version int
47}
48
49func (e errIncorrectFavoritesCacheVersion) Error() string {
50	return fmt.Sprintf("decoding of %s favorites cache failed: version was %d",
51		e.cache, e.version)
52}
53
54// favReq represents a request to access the logged-in user's
55// favorites list.  A single request can do one or more of the
56// following: refresh the current cached list, add a favorite, remove
57// a favorite, and get all the favorites.  When the request is done,
58// the resulting error (or nil) is sent over the done channel.  The
59// given ctx is used for all network operations.
60type favReq struct {
61	// Request types
62	clear              bool
63	refresh            bool
64	refreshID          tlf.ID
65	buffered           bool
66	toAdd              []favorites.ToAdd
67	toDel              []favorites.Folder
68	toGet              *favorites.Folder
69	folderWithFavFlags chan<- *keybase1.FolderWithFavFlags
70	favs               chan<- []favorites.Folder
71	favsAll            chan<- keybase1.FavoritesResult
72	homeTLFInfo        *homeTLFInfo
73
74	// For asynchronous refreshes, pass in the Favorites from the server here
75	favResult *keybase1.FavoritesResult
76
77	// Closed when the request is done.
78	done chan struct{}
79	// Set before done is closed
80	err error
81
82	// Context
83	ctx context.Context
84}
85
86type homeTLFInfo struct {
87	PublicTeamID  keybase1.TeamID
88	PrivateTeamID keybase1.TeamID
89}
90
91// Favorites manages a user's favorite list.
92type Favorites struct {
93	config           Config
94	disabled         bool
95	log              logger.Logger
96	bufferedInterval time.Duration
97
98	// homeTLFInfo stores the IDs for the logged-in user's home TLFs
99	homeTLFInfo homeTLFInfo
100
101	// Channel for interacting with the favorites cache
102	reqChan         chan *favReq
103	bufferedReqChan chan *favReq
104	// Channel that is full when there is already a refresh queued
105	refreshWaiting chan struct{}
106
107	wg         kbfssync.RepeatedWaitGroup
108	bufferedWg kbfssync.RepeatedWaitGroup
109	loopWG     kbfssync.RepeatedWaitGroup
110
111	// cache tracks the favorites for this user, that we know about.
112	// It may not be consistent with the server's view of the user's
113	// favorites list, if other devices have modified the list since
114	// the last refresh and this device is offline.
115	// When another device modifies the favorites [or new or ignored] list,
116	// the server will try to alert the other devices to refresh.
117	favCache        map[favorites.Folder]favorites.Data
118	newCache        map[favorites.Folder]favorites.Data
119	ignoredCache    map[favorites.Folder]favorites.Data
120	cacheExpireTime time.Time
121
122	diskCache *ldbutils.LevelDb
123
124	inFlightLock sync.Mutex
125	inFlightAdds map[favorites.Folder]*favReq
126
127	idLock        sync.Mutex
128	lastRefreshID tlf.ID
129
130	shutdownChan chan struct{}
131	muShutdown   sync.RWMutex
132	shutdown     bool
133}
134
135func newFavoritesWithChan(config Config, reqChan chan *favReq) *Favorites {
136	disableVal := strings.ToLower(os.Getenv(disableFavoritesEnvVar))
137	log := config.MakeLogger("FAV")
138	if len(disableVal) > 0 && disableVal != "0" && disableVal != "false" &&
139		disableVal != "no" {
140		log.CDebugf(
141			context.TODO(), "Disable favorites due to env var %s=%s",
142			disableFavoritesEnvVar, disableVal)
143		return &Favorites{
144			config:   config,
145			disabled: true,
146			log:      log,
147		}
148	}
149
150	f := &Favorites{
151		config:           config,
152		reqChan:          reqChan,
153		bufferedReqChan:  make(chan *favReq, 1),
154		refreshWaiting:   make(chan struct{}, 1),
155		inFlightAdds:     make(map[favorites.Folder]*favReq),
156		log:              log,
157		bufferedInterval: defaultFavoritesBufferedReqInterval,
158		shutdownChan:     make(chan struct{}),
159	}
160
161	return f
162}
163
164// NewFavorites constructs a new Favorites instance.
165func NewFavorites(config Config) *Favorites {
166	return newFavoritesWithChan(config, make(chan *favReq, 100))
167}
168
169type favoritesCacheForDisk struct {
170	Version      int
171	FavCache     map[favorites.Folder]favorites.Data
172	NewCache     map[favorites.Folder]favorites.Data
173	IgnoredCache map[favorites.Folder]favorites.Data
174}
175type favoritesCacheEncryptedForDisk struct {
176	Version        int
177	EncryptedCache []byte
178}
179
180func (f *Favorites) readCacheFromDisk(ctx context.Context) error {
181	// Read the encrypted cache from disk
182	var db *ldbutils.LevelDb
183	var err error
184	if f.config.IsTestMode() {
185		db, err = ldbutils.OpenLevelDb(storage.NewMemStorage(), f.config.Mode())
186	} else {
187		db, err = ldbutils.OpenVersionedLevelDb(f.log, f.config.StorageRoot(),
188			kbfsFavoritesCacheSubfolder, favoritesDiskCacheStorageVersion,
189			favoritesDiskCacheFilename, f.config.Mode())
190	}
191	if err != nil {
192		return err
193	}
194	f.diskCache = db
195	session, err := f.config.KBPKI().GetCurrentSession(ctx)
196	if err != nil {
197		return err
198	}
199	user := []byte(string(session.UID))
200	data, err := db.Get(user, nil)
201	if err == leveldb.ErrNotFound {
202		f.log.CInfof(ctx, "No favorites cache found for user %v", user)
203		return nil
204	} else if err != nil {
205		return err
206	}
207
208	// decode the data from the file and ensure its version is correct
209	var decodedData favoritesCacheEncryptedForDisk
210	err = f.config.Codec().Decode(data, &decodedData)
211	if err != nil {
212		return err
213	}
214	if decodedData.Version != favoritesDiskCacheStorageVersion {
215		return errIncorrectFavoritesCacheVersion{cache: "serialized",
216			version: decodedData.Version}
217	}
218
219	// Send the data to the service to be decrypted
220	decryptedData, err := f.config.KeybaseService().DecryptFavorites(ctx,
221		decodedData.EncryptedCache)
222	if err != nil {
223		return err
224	}
225
226	// Decode the data into the a map
227	var cacheDecoded favoritesCacheForDisk
228	err = f.config.Codec().Decode(decryptedData, &cacheDecoded)
229	if err != nil {
230		return err
231	}
232	if cacheDecoded.Version != favoritesDiskCacheVersion {
233		return errIncorrectFavoritesCacheVersion{cache: "encrypted",
234			version: decodedData.Version}
235	}
236
237	f.favCache = cacheDecoded.FavCache
238	f.newCache = cacheDecoded.NewCache
239	f.ignoredCache = cacheDecoded.IgnoredCache
240	return nil
241}
242
243func (f *Favorites) writeCacheToDisk(ctx context.Context) error {
244	if f.diskCache == nil {
245		return errNoFavoritesCache
246	}
247	// Encode the cache map into a byte buffer
248	cacheForDisk := favoritesCacheForDisk{
249		FavCache:     f.favCache,
250		NewCache:     f.newCache,
251		IgnoredCache: f.ignoredCache,
252		Version:      favoritesDiskCacheVersion,
253	}
254	cacheSerialized, err := f.config.Codec().Encode(cacheForDisk)
255	if err != nil {
256		return err
257	}
258
259	// Send the byte buffer to the service for encryption
260	data, err := f.config.KeybaseService().EncryptFavorites(ctx,
261		cacheSerialized)
262	if err != nil {
263		return err
264	}
265
266	// Encode the encrypted data in a versioned struct before writing it to
267	// the LevelDb.
268	cacheEncryptedForDisk := favoritesCacheEncryptedForDisk{
269		EncryptedCache: data,
270		Version:        favoritesDiskCacheStorageVersion,
271	}
272	encodedData, err := f.config.Codec().Encode(cacheEncryptedForDisk)
273	if err != nil {
274		return err
275	}
276
277	// Write the encrypted cache to disk
278	session, err := f.config.KBPKI().GetCurrentSession(ctx)
279	if err != nil {
280		return err
281	}
282	user := []byte(string(session.UID))
283	return f.diskCache.Put(user, encodedData, nil)
284}
285
286// InitForTest starts the Favorites cache's internal processing loop without
287// loading cached favorites from disk.
288func (f *Favorites) InitForTest() {
289	if f.disabled {
290		return
291	}
292	go f.loop()
293}
294
295// Initialize loads the favorites cache from disk and starts listening for
296// requests asynchronously.
297func (f *Favorites) Initialize(ctx context.Context) {
298	if f.disabled {
299		return
300	}
301	// load cache from disk
302	err := f.readCacheFromDisk(ctx)
303	if err != nil {
304		f.log.CWarningf(
305			ctx, "Failed to read cached favorites from disk: %v", err)
306	}
307
308	// launch background loop
309	go f.loop()
310}
311
312func (f *Favorites) closeReq(req *favReq, err error) {
313	f.inFlightLock.Lock()
314	defer f.inFlightLock.Unlock()
315	req.err = err
316	close(req.done)
317	for _, fav := range req.toAdd {
318		delete(f.inFlightAdds, fav.Folder)
319	}
320}
321
322func (f *Favorites) crossCheckWithEditHistory() {
323	// NOTE: Ideally we would wait until all edit activity processing
324	// had completed in the FBO before we do these checks, but I think
325	// in practice when the mtime changes, it'll be the edit activity
326	// processing that actually kicks off these favorites activity,
327	// and so that particular race won't be an issue.  If we see
328	// weirdness here though, it might be worth revisiting that
329	// assumption.
330
331	// The mtime attached to the favorites data returned by the API
332	// server is updated both on git activity, background collection,
333	// and pure deletion ops, none of which add new interesting
334	// content to the TLF.  So, fix the favorite times to be the
335	// latest known edit history times, if possible.  If not possible,
336	// that means the TLF is definitely not included in the latest
337	// list of TLF edit activity; so set these to be lower than the
338	// minimum known time, if they're not already.
339	var minTime keybase1.Time
340	uh := f.config.UserHistory()
341	tlfsWithNoHistory := make(map[favorites.Folder]favorites.Data)
342	for fav, data := range f.favCache {
343		h := uh.GetTlfHistory(tlf.CanonicalName(fav.Name), fav.Type)
344		if h.ServerTime == 0 {
345			if data.TlfMtime != nil {
346				tlfsWithNoHistory[fav] = data
347			}
348			continue
349		}
350		if minTime == 0 || h.ServerTime < minTime {
351			minTime = h.ServerTime
352		}
353		if data.TlfMtime == nil || *data.TlfMtime > h.ServerTime {
354			t := h.ServerTime
355			data.TlfMtime = &t
356			f.favCache[fav] = data
357		}
358	}
359
360	// Make sure all TLFs that aren't in the recent edit history get a
361	// timestamp that's smaller than the minimum time in the edit
362	// history.
363	if minTime > 0 {
364		for fav, data := range tlfsWithNoHistory {
365			if *data.TlfMtime > minTime {
366				t := minTime - 1
367				data.TlfMtime = &t
368				f.favCache[fav] = data
369			}
370		}
371	}
372}
373
374// sendChangesToEditHistory notes any deleted favorites and removes them
375// from this user's kbfsedits.UserHistory.
376func (f *Favorites) sendChangesToEditHistory(oldCache map[favorites.Folder]favorites.Data) (changed bool) {
377	for oldFav := range oldCache {
378		if _, present := f.favCache[oldFav]; !present {
379			f.config.UserHistory().ClearTLF(tlf.CanonicalName(oldFav.Name),
380				oldFav.Type)
381			changed = true
382		}
383	}
384	for newFav, newFavData := range f.favCache {
385		oldFavData, present := oldCache[newFav]
386		if !present {
387			f.config.KBFSOps().RefreshEditHistory(newFav)
388			changed = true
389		} else if newFavData.TlfMtime != nil &&
390			(oldFavData.TlfMtime == nil ||
391				(*newFavData.TlfMtime > *oldFavData.TlfMtime)) {
392			changed = true
393		}
394	}
395
396	return changed
397}
398
399func favoriteToFolder(fav favorites.Folder, data favorites.Data) keybase1.Folder {
400	return keybase1.Folder{
401		Name:         fav.Name,
402		Private:      data.Private,
403		Created:      false,
404		FolderType:   data.FolderType,
405		TeamID:       data.TeamID,
406		ResetMembers: data.ResetMembers,
407		Mtime:        data.TlfMtime,
408	}
409}
410
411func (f *Favorites) doIDRefresh(id tlf.ID) bool {
412	f.idLock.Lock()
413	defer f.idLock.Unlock()
414
415	if f.lastRefreshID == id {
416		return false
417	}
418	f.lastRefreshID = id
419	return true
420}
421
422func (f *Favorites) clearLastRefreshID() {
423	f.idLock.Lock()
424	defer f.idLock.Unlock()
425	f.lastRefreshID = tlf.NullID
426}
427
428func (f *Favorites) handleReq(req *favReq) (err error) {
429	defer f.wg.Done()
430
431	changed := false
432	defer func() {
433		f.closeReq(req, err)
434		if changed {
435			f.config.SubscriptionManagerPublisher().PublishChange(keybase1.SubscriptionTopic_FAVORITES)
436			f.config.Reporter().NotifyFavoritesChanged(req.ctx)
437		}
438	}()
439
440	if req.refresh && !req.buffered {
441		<-f.refreshWaiting
442	}
443
444	if req.refresh && req.refreshID != tlf.NullID &&
445		!f.doIDRefresh(req.refreshID) {
446		return nil
447	}
448
449	kbpki := f.config.KBPKI()
450	// Fetch a new list if:
451	//  (1) The user asked us to refresh
452	//  (2) We haven't fetched it before
453	//  (3) It's stale
454	//
455	// If just (3), use a short timeout so we can return the correct result
456	// quickly when offline.
457	needFetch := (req.refresh || f.favCache == nil) && !req.clear
458	wantFetch := f.config.Clock().Now().After(f.cacheExpireTime) && !req.clear
459
460	for _, fav := range req.toAdd {
461		// This check for adds is critical and we should definitely leave it
462		// in. We've had issues in the past with spamming the API server with
463		// adding the same favorite multiple times. We don't have the same
464		// problem with deletes, because after the user deletes it, they aren't
465		// accessing the folder again. But with adds, we could be going through
466		// this code on basically every folder access. Favorite deletes from
467		// another device result in a notification to this device, so a race
468		// condition where we miss an "add" can't happen.
469		_, present := f.favCache[fav.Folder]
470		if !fav.Created && present {
471			continue
472		}
473		err := kbpki.FavoriteAdd(req.ctx, fav.ToKBFolderHandle())
474		if err != nil {
475			f.log.CDebugf(req.ctx,
476				"Failure adding favorite %v: %v", fav, err)
477			return err
478		}
479		needFetch = true
480		changed = true
481	}
482
483	for _, fav := range req.toDel {
484		// Since our cache isn't necessarily up-to-date, always delete
485		// the favorite.
486		folder := fav.ToKBFolderHandle(false)
487		err := kbpki.FavoriteDelete(req.ctx, folder)
488		if err != nil {
489			return err
490		}
491		f.config.UserHistory().ClearTLF(tlf.CanonicalName(fav.Name), fav.Type)
492		changed = true
493		// Simply delete here instead of triggering another list as an
494		// optimization because there's nothing additional we need from core.
495		delete(f.favCache, fav)
496	}
497
498	if needFetch || wantFetch {
499		getCtx := req.ctx
500		if !needFetch {
501			var cancel context.CancelFunc
502			getCtx, cancel = context.WithTimeout(req.ctx,
503				favoritesServerTimeoutWhenCacheExpired)
504			defer cancel()
505		}
506		// Load the cache from the server. This possibly already happened
507		// asynchronously and was included in the request.
508		var favResult keybase1.FavoritesResult
509		if req.favResult == nil {
510			favResult, err = kbpki.FavoriteList(getCtx)
511		} else {
512			favResult = *req.favResult
513		}
514		if err != nil {
515			if needFetch {
516				// if we're supposed to refresh the cache and it's not
517				// working, mark the current cache expired.
518				now := f.config.Clock().Now()
519				if now.Before(f.cacheExpireTime) {
520					f.cacheExpireTime = now
521				}
522				return err
523			}
524			// If we weren't explicitly asked to refresh, we can return possibly
525			// stale favorites rather than return nothing.
526			if err == context.DeadlineExceeded {
527				newCtx, _ := context.WithTimeout(context.Background(),
528					favoritesBackgroundRefreshTimeout)
529				go f.RefreshCache(newCtx, FavoritesRefreshModeBlocking)
530			}
531			f.log.CDebugf(req.ctx,
532				"Serving possibly stale favorites; new data could not be"+
533					" fetched: %v", err)
534		} else { // Successfully got new favorites from server.
535			if req.refreshID == tlf.NullID {
536				f.clearLastRefreshID()
537			}
538
539			session, sessionErr := kbpki.GetCurrentSession(req.ctx)
540			oldCache := f.favCache
541			f.newCache = make(map[favorites.Folder]favorites.Data)
542			f.favCache = make(map[favorites.Folder]favorites.Data)
543			f.ignoredCache = make(map[favorites.Folder]favorites.Data)
544			f.cacheExpireTime = libkb.ForceWallClock(f.config.Clock().Now()).Add(
545				favoritesCacheExpirationTime)
546			for _, folder := range favResult.FavoriteFolders {
547				f.favCache[*favorites.NewFolderFromProtocol(
548					folder)] = favorites.DataFrom(folder)
549				if sessionErr != nil && folder.Name == string(session.Name) {
550					if folder.Private {
551						f.homeTLFInfo.PrivateTeamID = *folder.TeamID
552					} else {
553						f.homeTLFInfo.PublicTeamID = *folder.TeamID
554					}
555				}
556			}
557			f.crossCheckWithEditHistory()
558			for _, folder := range favResult.IgnoredFolders {
559				f.ignoredCache[*favorites.NewFolderFromProtocol(
560					folder)] = favorites.DataFrom(folder)
561			}
562			for _, folder := range favResult.NewFolders {
563				f.newCache[*favorites.NewFolderFromProtocol(
564					folder)] = favorites.DataFrom(folder)
565			}
566			if sessionErr == nil {
567				// Add favorites for the current user, that cannot be
568				// deleted.  Only overwrite them (with a 0 mtime) if
569				// they weren't already part of the favorites list.
570				selfPriv := favorites.Folder{
571					Name: string(session.Name),
572					Type: tlf.Private,
573				}
574				if _, ok := f.favCache[selfPriv]; !ok {
575					f.favCache[selfPriv] = favorites.Data{
576						Name:       string(session.Name),
577						FolderType: tlf.Private.FolderType(),
578						TeamID:     &f.homeTLFInfo.PrivateTeamID,
579						Private:    true,
580					}
581				}
582				selfPub := favorites.Folder{
583					Name: string(session.Name),
584					Type: tlf.Public,
585				}
586				if _, ok := f.favCache[selfPub]; !ok {
587					f.favCache[selfPub] = favorites.Data{
588						Name:       string(session.Name),
589						FolderType: tlf.Public.FolderType(),
590						TeamID:     &f.homeTLFInfo.PublicTeamID,
591						Private:    false,
592					}
593				}
594				err = f.writeCacheToDisk(req.ctx)
595				if err != nil {
596					f.log.CWarningf(req.ctx,
597						"Could not write favorites to disk cache: %v", err)
598				}
599			}
600			if oldCache != nil {
601				changed = f.sendChangesToEditHistory(oldCache)
602			}
603		}
604	} else if req.clear {
605		f.favCache = nil
606		changed = true
607		return nil
608	}
609
610	if req.favs != nil {
611		favorites := make([]favorites.Folder, 0, len(f.favCache))
612		for fav := range f.favCache {
613			favorites = append(favorites, fav)
614		}
615		req.favs <- favorites
616	}
617
618	if req.favsAll != nil {
619		favFolders := make([]keybase1.Folder, 0, len(f.favCache))
620		newFolders := make([]keybase1.Folder, 0, len(f.newCache))
621		ignoredFolders := make([]keybase1.Folder, 0, len(f.ignoredCache))
622
623		for fav, data := range f.favCache {
624			favFolders = append(favFolders, favoriteToFolder(fav, data))
625		}
626		for fav, data := range f.newCache {
627			newFolders = append(newFolders, favoriteToFolder(fav, data))
628		}
629		for fav, data := range f.ignoredCache {
630			ignoredFolders = append(ignoredFolders, favoriteToFolder(fav, data))
631		}
632
633		req.favsAll <- keybase1.FavoritesResult{
634			NewFolders:      newFolders,
635			IgnoredFolders:  ignoredFolders,
636			FavoriteFolders: favFolders,
637		}
638	}
639
640	if req.folderWithFavFlags != nil && req.toGet != nil {
641		fav := *req.toGet
642		if data, ok := f.favCache[fav]; ok {
643			req.folderWithFavFlags <- &keybase1.FolderWithFavFlags{
644				Folder:     favoriteToFolder(fav, data),
645				IsFavorite: true,
646			}
647		} else if data, ok := f.newCache[*req.toGet]; ok {
648			req.folderWithFavFlags <- &keybase1.FolderWithFavFlags{
649				Folder: favoriteToFolder(fav, data),
650				IsNew:  true,
651			}
652		} else if data, ok := f.ignoredCache[*req.toGet]; ok {
653			req.folderWithFavFlags <- &keybase1.FolderWithFavFlags{
654				Folder:    favoriteToFolder(fav, data),
655				IsIgnored: true,
656			}
657		} else {
658			req.folderWithFavFlags <- nil
659		}
660	}
661
662	if req.homeTLFInfo != nil {
663		f.homeTLFInfo = *req.homeTLFInfo
664	}
665
666	return nil
667}
668
669func (f *Favorites) loop() {
670	f.loopWG.Add(1)
671	defer f.loopWG.Done()
672	bufferedTicker := time.NewTicker(f.bufferedInterval)
673	defer bufferedTicker.Stop()
674
675	for {
676		select {
677		case req, ok := <-f.reqChan:
678			if !ok {
679				return
680			}
681			err := f.handleReq(req)
682			if err != nil {
683				f.log.CDebugf(
684					context.TODO(), "Error handling request: %+v", err)
685			}
686		case <-bufferedTicker.C:
687			select {
688			case req, ok := <-f.bufferedReqChan:
689				if !ok {
690					// Still need to close out any regular requests.
691					continue
692				}
693				// Don't block the wait group on buffered requests
694				// until we're actually processing one.
695				f.wg.Add(1)
696				err := f.handleReq(req)
697				if err != nil {
698					f.log.CDebugf(
699						context.TODO(), "Error handling request: %+v", err)
700				}
701			default:
702			}
703		}
704	}
705}
706
707// Shutdown shuts down this Favorites instance.
708func (f *Favorites) Shutdown() error {
709	if f.disabled {
710		return nil
711	}
712
713	f.muShutdown.Lock()
714	defer f.muShutdown.Unlock()
715	f.shutdown = true
716	close(f.reqChan)
717	close(f.bufferedReqChan)
718	close(f.shutdownChan)
719	if f.diskCache != nil {
720		err := f.diskCache.Close()
721		if err != nil {
722			f.log.CWarningf(context.Background(),
723				"Could not close disk favorites cache: %v", err)
724		}
725	}
726	err := f.wg.Wait(context.Background())
727	if err != nil {
728		return err
729	}
730	return f.loopWG.Wait(context.Background())
731}
732
733func (f *Favorites) waitOnReq(ctx context.Context,
734	req *favReq) (retry bool, err error) {
735	select {
736	case <-ctx.Done():
737		return false, ctx.Err()
738	case <-req.done:
739		err = req.err
740		// If the request was canceled due to a context timeout that
741		// wasn't our own, try it again.
742		if err == context.Canceled || err == context.DeadlineExceeded {
743			select {
744			case <-ctx.Done():
745				return false, err
746			default:
747				return true, nil
748			}
749		}
750		return false, err
751	}
752}
753
754func (f *Favorites) sendReq(ctx context.Context, req *favReq) error {
755	f.wg.Add(1)
756	select {
757	case f.reqChan <- req:
758	case <-ctx.Done():
759		f.wg.Done()
760		err := ctx.Err()
761		f.closeReq(req, err)
762		return err
763	}
764	// With a direct sendReq call, we'll never have a shared request,
765	// so no need to check the retry status.
766	_, err := f.waitOnReq(ctx, req)
767	return err
768}
769
770func (f *Favorites) startOrJoinAddReq(
771	ctx context.Context, fav favorites.ToAdd) (req *favReq, doSend bool) {
772	f.inFlightLock.Lock()
773	defer f.inFlightLock.Unlock()
774	req, ok := f.inFlightAdds[fav.Folder]
775	if !ok {
776		req = &favReq{
777			ctx:   ctx,
778			toAdd: []favorites.ToAdd{fav},
779			done:  make(chan struct{}),
780		}
781		f.inFlightAdds[fav.Folder] = req
782		doSend = true
783	}
784	return req, doSend
785}
786
787// Add adds a favorite to your favorites list.
788func (f *Favorites) Add(ctx context.Context, fav favorites.ToAdd) error {
789	f.muShutdown.RLock()
790	defer f.muShutdown.RUnlock()
791
792	if f.disabled {
793		return nil
794	}
795	if f.shutdown {
796		return data.ShutdownHappenedError{}
797	}
798	doAdd := true
799	var err error
800	// Retry until we get an error that wasn't related to someone
801	// else's context being canceled.
802	for doAdd {
803		req, doSend := f.startOrJoinAddReq(ctx, fav)
804		if doSend {
805			return f.sendReq(ctx, req)
806		}
807		doAdd, err = f.waitOnReq(ctx, req)
808	}
809	return err
810}
811
812// AddAsync initiates a request to add this favorite to your favorites
813// list, if one is not already in flight, but it doesn't wait for the
814// result.  (It could block while kicking off the request, if lots of
815// different favorite operations are in flight.)  The given context is
816// used only for enqueuing the request on an internal queue, not for
817// any resulting I/O.
818func (f *Favorites) AddAsync(ctx context.Context, fav favorites.ToAdd) {
819	f.muShutdown.RLock()
820	defer f.muShutdown.RUnlock()
821
822	if f.disabled || f.shutdown {
823		return
824	}
825	// Use a fresh context, since we want the request to succeed even
826	// if the original context is canceled.
827	req, doSend := f.startOrJoinAddReq(context.Background(), fav)
828	if doSend {
829		f.wg.Add(1)
830		select {
831		case f.reqChan <- req:
832		case <-ctx.Done():
833			f.wg.Done()
834			err := ctx.Err()
835			f.closeReq(req, err)
836			return
837		}
838	}
839}
840
841// Delete deletes a favorite from the favorites list.  It is
842// idempotent.
843func (f *Favorites) Delete(ctx context.Context, fav favorites.Folder) error {
844	f.muShutdown.RLock()
845	defer f.muShutdown.RUnlock()
846
847	if f.disabled {
848		return nil
849	}
850	if f.shutdown {
851		return data.ShutdownHappenedError{}
852	}
853	return f.sendReq(ctx, &favReq{
854		ctx:   ctx,
855		toDel: []favorites.Folder{fav},
856		done:  make(chan struct{}),
857	})
858}
859
860// FavoritesRefreshMode controls how a favorites refresh happens.
861type FavoritesRefreshMode int
862
863const (
864	// FavoritesRefreshModeInMainFavoritesLoop means to refresh the favorites
865	// in the main loop, blocking any favorites requests after until the refresh
866	// is done.
867	FavoritesRefreshModeInMainFavoritesLoop = iota
868	// FavoritesRefreshModeBlocking means to refresh the favorites outside
869	// of the main loop.
870	FavoritesRefreshModeBlocking
871)
872
873// RefreshCache refreshes the cached list of favorites.
874//
875// In FavoritesRefreshModeBlocking, request the favorites in this function,
876// then send them to the main goroutine to be processed. This should be called
877// in a separate goroutine from anything mission-critical, because it might wait
878// on network for up to 15 seconds.
879//
880// In FavoritesRefreshModeInMainFavoritesLoop, this just sets up a request and
881// sends it to the main goroutine to process it - this is useful if e.g.
882// the favorites cache has not been initialized at all and cannot serve any
883// requests until this refresh is completed.
884func (f *Favorites) RefreshCache(ctx context.Context, mode FavoritesRefreshMode) {
885	f.muShutdown.RLock()
886	defer f.muShutdown.RUnlock()
887	if f.disabled || f.shutdown {
888		return
889	}
890
891	// Insert something into the refreshWaiting channel to guarantee that we
892	// are the only current refresh.
893	select {
894	case f.refreshWaiting <- struct{}{}:
895	default:
896		// There is already a refresh in the queue
897		return
898	}
899	// This request is non-blocking, so use a throw-away done channel
900	// and context. Note that in the `blocking` mode, this context will only
901	// be relevant for the brief moment the main loop processes the results
902	// generated in the below network request.
903	req := &favReq{
904		refresh: true,
905		done:    make(chan struct{}),
906		ctx:     context.Background(),
907	}
908	f.wg.Add(1)
909
910	if mode == FavoritesRefreshModeBlocking {
911		favResult, err := f.config.KBPKI().FavoriteList(ctx)
912		if err != nil {
913			f.log.CDebugf(ctx, "Failed to refresh cached Favorites: %+v", err)
914			// Because the request will not make it to the main processing
915			// loop, mark it as done and clear the refresh channel here.
916			f.wg.Done()
917			<-f.refreshWaiting
918			return
919		}
920		req.favResult = &favResult
921	}
922	select {
923	case f.reqChan <- req:
924		go func() {
925			<-req.done
926			if req.err != nil {
927				f.log.CDebugf(ctx, "Failed to refresh cached Favorites ("+
928					"error in main loop): %+v", req.err)
929			}
930		}()
931	case <-ctx.Done():
932		// Because the request will not make it to the main processing
933		// loop, mark it as done and clear the refresh channel here.
934		f.wg.Done()
935		<-f.refreshWaiting
936		return
937	}
938}
939
940// RefreshCacheWhenMTimeChanged refreshes the cached favorites, but
941// does so with rate-limiting, so that it doesn't hit the server too
942// often.  `id` is the ID of the TLF that caused this refresh to
943// happen.  As an optimization, if `id` matches the previous `id` that
944// triggered a refresh, then we just ignore the refresh since it won't
945// materially change the order of the favorites by mtime.
946func (f *Favorites) RefreshCacheWhenMTimeChanged(
947	ctx context.Context, id tlf.ID) {
948	f.muShutdown.RLock()
949	defer f.muShutdown.RUnlock()
950	if f.disabled || f.shutdown {
951		return
952	}
953
954	req := &favReq{
955		refresh:   true,
956		refreshID: id,
957		buffered:  true,
958		done:      make(chan struct{}),
959		ctx:       context.Background(),
960	}
961	f.bufferedWg.Add(1)
962	select {
963	case f.bufferedReqChan <- req:
964		go func() {
965			defer f.bufferedWg.Done()
966			select {
967			case <-req.done:
968				if req.err != nil {
969					f.log.CDebugf(ctx, "Failed to refresh cached Favorites ("+
970						"error in main loop): %+v", req.err)
971				}
972			case <-f.shutdownChan:
973			}
974		}()
975	default:
976		// There's already a buffered request waiting.
977		f.bufferedWg.Done()
978	}
979}
980
981// ClearCache clears the cached list of favorites.
982func (f *Favorites) ClearCache(ctx context.Context) {
983	f.muShutdown.RLock()
984	defer f.muShutdown.RUnlock()
985	if f.disabled || f.shutdown {
986		return
987	}
988	// This request is non-blocking, so use a throw-away done channel
989	// and context.
990	req := &favReq{
991		clear:   true,
992		refresh: false,
993		done:    make(chan struct{}),
994		ctx:     context.Background(),
995	}
996	f.wg.Add(1)
997	select {
998	case f.reqChan <- req:
999	case <-ctx.Done():
1000		f.wg.Done()
1001		return
1002	}
1003}
1004
1005// GetFolderWithFavFlags returns the a FolderWithFavFlags for give folder, if found.
1006func (f *Favorites) GetFolderWithFavFlags(
1007	ctx context.Context, fav favorites.Folder) (
1008	folderWithFavFlags *keybase1.FolderWithFavFlags, errr error) {
1009	if f.disabled {
1010		return nil, nil
1011	}
1012	f.muShutdown.RLock()
1013	defer f.muShutdown.RUnlock()
1014	if f.shutdown {
1015		return nil, data.ShutdownHappenedError{}
1016	}
1017	ch := make(chan *keybase1.FolderWithFavFlags, 1)
1018	req := &favReq{
1019		ctx:                ctx,
1020		toGet:              &fav,
1021		folderWithFavFlags: ch,
1022		done:               make(chan struct{}),
1023	}
1024	err := f.sendReq(ctx, req)
1025	if err != nil {
1026		return nil, err
1027	}
1028	folderWithFavFlags = <-ch
1029	return folderWithFavFlags, nil
1030}
1031
1032// Get returns the logged-in user's list of favorites. It uses the cache.
1033func (f *Favorites) Get(ctx context.Context) ([]favorites.Folder, error) {
1034	if f.disabled {
1035		session, err := f.config.KBPKI().GetCurrentSession(ctx)
1036		if err == nil {
1037			// Add favorites only for the current user.
1038			return []favorites.Folder{
1039				{Name: string(session.Name), Type: tlf.Private},
1040				{Name: string(session.Name), Type: tlf.Public},
1041			}, nil
1042		}
1043		return nil, nil
1044	}
1045	f.muShutdown.RLock()
1046	defer f.muShutdown.RUnlock()
1047	if f.shutdown {
1048		return nil, data.ShutdownHappenedError{}
1049	}
1050	favChan := make(chan []favorites.Folder, 1)
1051	req := &favReq{
1052		ctx:  ctx,
1053		favs: favChan,
1054		done: make(chan struct{}),
1055	}
1056	err := f.sendReq(ctx, req)
1057	if err != nil {
1058		return nil, err
1059	}
1060	return <-favChan, nil
1061}
1062
1063// setHomeTLFInfo should be called when a new user logs in so that their home
1064// TLFs can be returned as favorites.
1065func (f *Favorites) setHomeTLFInfo(ctx context.Context, info homeTLFInfo) {
1066	f.muShutdown.RLock()
1067	defer f.muShutdown.RUnlock()
1068	if f.shutdown {
1069		return
1070	}
1071	// This request is non-blocking, so use a throw-away done channel
1072	// and context.
1073	req := &favReq{
1074		homeTLFInfo: &info,
1075		done:        make(chan struct{}),
1076		ctx:         context.Background(),
1077	}
1078	f.wg.Add(1)
1079	select {
1080	case f.reqChan <- req:
1081	case <-ctx.Done():
1082		f.wg.Done()
1083		return
1084	}
1085}
1086
1087// GetAll returns the logged-in user's list of favorite, new, and ignored TLFs.
1088// It uses the cache.
1089func (f *Favorites) GetAll(ctx context.Context) (keybase1.FavoritesResult,
1090	error) {
1091	if f.disabled {
1092		session, err := f.config.KBPKI().GetCurrentSession(ctx)
1093		if err == nil {
1094			// Add favorites only for the current user.
1095			return keybase1.FavoritesResult{
1096				FavoriteFolders: []keybase1.Folder{
1097					{
1098						Name:       string(session.Name),
1099						Private:    false,
1100						Created:    false,
1101						FolderType: keybase1.FolderType_PUBLIC,
1102						TeamID:     &f.homeTLFInfo.PublicTeamID,
1103					},
1104					{
1105						Name:       string(session.Name),
1106						Private:    true,
1107						Created:    false,
1108						FolderType: keybase1.FolderType_PRIVATE,
1109						TeamID:     &f.homeTLFInfo.PrivateTeamID,
1110					},
1111				},
1112			}, nil
1113		}
1114		return keybase1.FavoritesResult{}, nil
1115	}
1116	f.muShutdown.RLock()
1117	defer f.muShutdown.RUnlock()
1118
1119	if f.shutdown {
1120		return keybase1.FavoritesResult{}, data.ShutdownHappenedError{}
1121	}
1122	favChan := make(chan keybase1.FavoritesResult, 1)
1123	req := &favReq{
1124		ctx:     ctx,
1125		favsAll: favChan,
1126		done:    make(chan struct{}),
1127	}
1128	err := f.sendReq(ctx, req)
1129	if err != nil {
1130		return keybase1.FavoritesResult{}, err
1131	}
1132	return <-favChan, nil
1133}
1134