1package avatars
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"io"
8	"io/ioutil"
9	"net/url"
10	"os"
11	"path/filepath"
12	"sync"
13	"time"
14
15	"github.com/keybase/client/go/libkb"
16	"github.com/keybase/client/go/lru"
17	"github.com/keybase/client/go/protocol/keybase1"
18)
19
20type avatarLoadPair struct {
21	name      string
22	format    keybase1.AvatarFormat
23	path      string
24	remoteURL *string
25}
26
27type avatarLoadSpec struct {
28	hits   []avatarLoadPair
29	misses []avatarLoadPair
30	stales []avatarLoadPair
31}
32
33func (a avatarLoadSpec) details(l []avatarLoadPair) (names []string, formats []keybase1.AvatarFormat) {
34	fmap := make(map[keybase1.AvatarFormat]bool)
35	umap := make(map[string]bool)
36	for _, m := range l {
37		umap[m.name] = true
38		fmap[m.format] = true
39	}
40	for u := range umap {
41		names = append(names, u)
42	}
43	for f := range fmap {
44		formats = append(formats, f)
45	}
46	return names, formats
47}
48
49func (a avatarLoadSpec) missDetails() ([]string, []keybase1.AvatarFormat) {
50	return a.details(a.misses)
51}
52
53func (a avatarLoadSpec) staleDetails() ([]string, []keybase1.AvatarFormat) {
54	return a.details(a.stales)
55}
56
57func (a avatarLoadSpec) staleKnownURL(name string, format keybase1.AvatarFormat) *string {
58	for _, stale := range a.stales {
59		if stale.name == name && stale.format == format {
60			return stale.remoteURL
61		}
62	}
63	return nil
64}
65
66type populateArg struct {
67	name   string
68	format keybase1.AvatarFormat
69	url    keybase1.AvatarUrl
70}
71
72type remoteFetchArg struct {
73	names   []string
74	formats []keybase1.AvatarFormat
75	cb      chan keybase1.LoadAvatarsRes
76	errCb   chan error
77}
78
79type lruEntry struct {
80	Path string
81	URL  *string
82}
83
84func (l lruEntry) GetPath() string {
85	return l.Path
86}
87
88type FullCachingSource struct {
89	libkb.Contextified
90	sync.Mutex
91	started              bool
92	diskLRU              *lru.DiskLRU
93	diskLRUCleanerCancel context.CancelFunc
94	staleThreshold       time.Duration
95	simpleSource         libkb.AvatarLoaderSource
96
97	populateCacheCh chan populateArg
98
99	prepareDirs sync.Once
100
101	usersMissBatch  func(interface{})
102	teamsMissBatch  func(interface{})
103	usersStaleBatch func(interface{})
104	teamsStaleBatch func(interface{})
105
106	// testing
107	populateSuccessCh chan struct{}
108	tempDir           string
109}
110
111var _ libkb.AvatarLoaderSource = (*FullCachingSource)(nil)
112
113func NewFullCachingSource(g *libkb.GlobalContext, staleThreshold time.Duration, size int) *FullCachingSource {
114	s := &FullCachingSource{
115		Contextified:   libkb.NewContextified(g),
116		diskLRU:        lru.NewDiskLRU("avatars", 1, size),
117		staleThreshold: staleThreshold,
118		simpleSource:   NewSimpleSource(),
119	}
120	batcher := func(intBatched interface{}, intSingle interface{}) interface{} {
121		reqs, _ := intBatched.([]remoteFetchArg)
122		single, _ := intSingle.(remoteFetchArg)
123		return append(reqs, single)
124	}
125	reset := func() interface{} {
126		return []remoteFetchArg{}
127	}
128	actor := func(loadFn func(libkb.MetaContext, []string, []keybase1.AvatarFormat) (keybase1.LoadAvatarsRes, error)) func(interface{}) {
129		return func(intBatched interface{}) {
130			reqs, _ := intBatched.([]remoteFetchArg)
131			s.makeRemoteFetchRequests(reqs, loadFn)
132		}
133	}
134	usersMissBatch, _ := libkb.ThrottleBatch(
135		actor(s.simpleSource.LoadUsers), batcher, reset, 100*time.Millisecond, false,
136	)
137	teamsMissBatch, _ := libkb.ThrottleBatch(
138		actor(s.simpleSource.LoadTeams), batcher, reset, 100*time.Millisecond, false,
139	)
140	usersStaleBatch, _ := libkb.ThrottleBatch(
141		actor(s.simpleSource.LoadUsers), batcher, reset, 5000*time.Millisecond, false,
142	)
143	teamsStaleBatch, _ := libkb.ThrottleBatch(
144		actor(s.simpleSource.LoadTeams), batcher, reset, 5000*time.Millisecond, false,
145	)
146	s.usersMissBatch = usersMissBatch
147	s.teamsMissBatch = teamsMissBatch
148	s.usersStaleBatch = usersStaleBatch
149	s.teamsStaleBatch = teamsStaleBatch
150	return s
151}
152
153func (c *FullCachingSource) makeRemoteFetchRequests(reqs []remoteFetchArg,
154	loadFn func(libkb.MetaContext, []string, []keybase1.AvatarFormat) (keybase1.LoadAvatarsRes, error)) {
155	mctx := libkb.NewMetaContextBackground(c.G())
156	namesSet := make(map[string]bool)
157	formatsSet := make(map[keybase1.AvatarFormat]bool)
158	for _, req := range reqs {
159		for _, name := range req.names {
160			namesSet[name] = true
161		}
162		for _, format := range req.formats {
163			formatsSet[format] = true
164		}
165	}
166	genErrors := func(err error) {
167		for _, req := range reqs {
168			req.errCb <- err
169		}
170	}
171	extractRes := func(req remoteFetchArg, ires keybase1.LoadAvatarsRes) (res keybase1.LoadAvatarsRes) {
172		res.Picmap = make(map[string]map[keybase1.AvatarFormat]keybase1.AvatarUrl)
173		for _, name := range req.names {
174			iformats, ok := ires.Picmap[name]
175			if !ok {
176				continue
177			}
178			if _, ok := res.Picmap[name]; !ok {
179				res.Picmap[name] = make(map[keybase1.AvatarFormat]keybase1.AvatarUrl)
180			}
181			for _, format := range req.formats {
182				res.Picmap[name][format] = iformats[format]
183			}
184		}
185		return res
186	}
187	names := make([]string, 0, len(namesSet))
188	formats := make([]keybase1.AvatarFormat, 0, len(formatsSet))
189	for name := range namesSet {
190		names = append(names, name)
191	}
192	for format := range formatsSet {
193		formats = append(formats, format)
194	}
195	c.debug(mctx, "makeRemoteFetchRequests: names: %d formats: %d", len(names), len(formats))
196	res, err := loadFn(mctx, names, formats)
197	if err != nil {
198		genErrors(err)
199		return
200	}
201	for _, req := range reqs {
202		req.cb <- extractRes(req, res)
203	}
204}
205
206func (c *FullCachingSource) StartBackgroundTasks(mctx libkb.MetaContext) {
207	defer mctx.Trace("FullCachingSource.StartBackgroundTasks", nil)()
208	c.Lock()
209	defer c.Unlock()
210	if c.started {
211		return
212	}
213	c.started = true
214	go c.monitorAppState(mctx)
215	c.populateCacheCh = make(chan populateArg, 100)
216	for i := 0; i < 10; i++ {
217		go c.populateCacheWorker(mctx)
218	}
219	mctx, cancel := mctx.WithContextCancel()
220	c.diskLRUCleanerCancel = cancel
221	go lru.CleanOutOfSyncWithDelay(mctx, c.diskLRU, c.getCacheDir(mctx), 10*time.Second)
222}
223
224func (c *FullCachingSource) StopBackgroundTasks(mctx libkb.MetaContext) {
225	defer mctx.Trace("FullCachingSource.StopBackgroundTasks", nil)()
226	c.Lock()
227	defer c.Unlock()
228	if !c.started {
229		return
230	}
231	c.started = false
232	close(c.populateCacheCh)
233	if c.diskLRUCleanerCancel != nil {
234		c.diskLRUCleanerCancel()
235	}
236	if err := c.diskLRU.Flush(mctx.Ctx(), mctx.G()); err != nil {
237		c.debug(mctx, "StopBackgroundTasks: unable to flush diskLRU %v", err)
238	}
239}
240
241func (c *FullCachingSource) debug(m libkb.MetaContext, msg string, args ...interface{}) {
242	m.Debug("Avatars.FullCachingSource: %s", fmt.Sprintf(msg, args...))
243}
244
245func (c *FullCachingSource) avatarKey(name string, format keybase1.AvatarFormat) string {
246	return fmt.Sprintf("%s:%s", name, format.String())
247}
248
249func (c *FullCachingSource) isStale(m libkb.MetaContext, item lru.DiskLRUEntry) bool {
250	return m.G().GetClock().Now().Sub(item.Ctime) > c.staleThreshold
251}
252
253func (c *FullCachingSource) monitorAppState(m libkb.MetaContext) {
254	c.debug(m, "monitorAppState: starting up")
255	state := keybase1.MobileAppState_FOREGROUND
256	for {
257		state = <-m.G().MobileAppState.NextUpdate(&state)
258		if state == keybase1.MobileAppState_BACKGROUND {
259			c.debug(m, "monitorAppState: backgrounded")
260			if err := c.diskLRU.Flush(m.Ctx(), m.G()); err != nil {
261				c.debug(m, "monitorAppState: unable to flush diskLRU %v", err)
262			}
263		}
264	}
265}
266
267func (c *FullCachingSource) processLRUHit(entry lru.DiskLRUEntry) (res lruEntry) {
268	var ok bool
269	if _, ok = entry.Value.(map[string]interface{}); ok {
270		jstr, _ := json.Marshal(entry.Value)
271		_ = json.Unmarshal(jstr, &res)
272		return res
273	}
274	path, _ := entry.Value.(string)
275	res.Path = path
276	return res
277}
278
279func (c *FullCachingSource) specLoad(m libkb.MetaContext, names []string, formats []keybase1.AvatarFormat) (res avatarLoadSpec, err error) {
280	for _, name := range names {
281		for _, format := range formats {
282			key := c.avatarKey(name, format)
283			found, ientry, err := c.diskLRU.Get(m.Ctx(), m.G(), key)
284			if err != nil {
285				return res, err
286			}
287			lp := avatarLoadPair{
288				name:   name,
289				format: format,
290			}
291
292			// If we found something in the index, let's make sure we have it on the disk as well.
293			entry := c.processLRUHit(ientry)
294			if found {
295				lp.path = c.normalizeFilenameFromCache(m, entry.Path)
296				lp.remoteURL = entry.URL
297				var file *os.File
298				if file, err = os.Open(lp.path); err != nil {
299					c.debug(m, "specLoad: error loading hit: file: %s err: %s", lp.path, err)
300					if err := c.diskLRU.Remove(m.Ctx(), m.G(), key); err != nil {
301						c.debug(m, "specLoad: unable to remove from LRU %v", err)
302					}
303					// Not a true hit if we don't have it on the disk as well
304					found = false
305				} else {
306					file.Close()
307				}
308			}
309			if found {
310				if c.isStale(m, ientry) {
311					res.stales = append(res.stales, lp)
312				} else {
313					res.hits = append(res.hits, lp)
314				}
315			} else {
316				res.misses = append(res.misses, lp)
317			}
318		}
319	}
320	return res, nil
321}
322
323func (c *FullCachingSource) getCacheDir(m libkb.MetaContext) string {
324	if len(c.tempDir) > 0 {
325		return c.tempDir
326	}
327	return filepath.Join(m.G().GetCacheDir(), "avatars")
328}
329
330func (c *FullCachingSource) getFullFilename(fileName string) string {
331	return fileName + ".avatar"
332}
333
334// normalizeFilenameFromCache substitutes the existing cache dir value into the
335// file path since it's possible for the path to the cache dir to change,
336// especially on mobile.
337func (c *FullCachingSource) normalizeFilenameFromCache(mctx libkb.MetaContext, file string) string {
338	file = filepath.Base(file)
339	return filepath.Join(c.getCacheDir(mctx), file)
340}
341
342func (c *FullCachingSource) commitAvatarToDisk(m libkb.MetaContext, data io.ReadCloser, previousPath string) (path string, err error) {
343	c.prepareDirs.Do(func() {
344		err := os.MkdirAll(c.getCacheDir(m), os.ModePerm)
345		c.debug(m, "creating directory for avatars %q: %v", c.getCacheDir(m), err)
346	})
347
348	var file *os.File
349	shouldRename := false
350	if len(previousPath) > 0 {
351		// We already have the image, let's re-use the same file
352		c.debug(m, "commitAvatarToDisk: using previous path: %s", previousPath)
353		if file, err = os.OpenFile(previousPath, os.O_RDWR, os.ModeAppend); err != nil {
354			// NOTE: Even if we don't have this file anymore (e.g. user
355			// raced us to remove it manually), OpenFile will not error
356			// out, but create a new file on given path.
357			return path, err
358		}
359		path = file.Name()
360	} else {
361		if file, err = ioutil.TempFile(c.getCacheDir(m), "avatar"); err != nil {
362			return path, err
363		}
364		shouldRename = true
365	}
366	_, err = io.Copy(file, data)
367	file.Close()
368	if err != nil {
369		return path, err
370	}
371	// Rename with correct extension
372	if shouldRename {
373		path = c.getFullFilename(file.Name())
374		if err = os.Rename(file.Name(), path); err != nil {
375			return path, err
376		}
377	}
378	return path, nil
379}
380
381func (c *FullCachingSource) removeFile(m libkb.MetaContext, ent *lru.DiskLRUEntry) {
382	if ent != nil {
383		lentry := c.processLRUHit(*ent)
384		file := c.normalizeFilenameFromCache(m, lentry.GetPath())
385		if err := os.Remove(file); err != nil {
386			c.debug(m, "removeFile: failed to remove: file: %s err: %s", file, err)
387		} else {
388			c.debug(m, "removeFile: successfully removed: %s", file)
389		}
390	}
391}
392
393func (c *FullCachingSource) populateCacheWorker(m libkb.MetaContext) {
394	for arg := range c.populateCacheCh {
395		c.debug(m, "populateCacheWorker: fetching: name: %s format: %s url: %s", arg.name,
396			arg.format, arg.url)
397		// Grab image data first
398		url := arg.url.String()
399		resp, err := libkb.ProxyHTTPGet(m.G(), m.G().GetEnv(), url, "FullCachingSource: Avatar")
400		if err != nil {
401			c.debug(m, "populateCacheWorker: failed to download avatar: %s", err)
402			continue
403		}
404		// Find any previous path we stored this image at on the disk
405		var previousEntry lruEntry
406		var previousPath string
407		key := c.avatarKey(arg.name, arg.format)
408		found, ent, err := c.diskLRU.Get(m.Ctx(), m.G(), key)
409		if err != nil {
410			c.debug(m, "populateCacheWorker: failed to read previous entry in LRU: %s", err)
411			err = libkb.DiscardAndCloseBody(resp)
412			if err != nil {
413				c.debug(m, "populateCacheWorker: error closing body: %+v", err)
414			}
415			continue
416		}
417		if found {
418			previousEntry = c.processLRUHit(ent)
419			previousPath = c.normalizeFilenameFromCache(m, previousEntry.Path)
420		}
421
422		// Save to disk
423		path, err := c.commitAvatarToDisk(m, resp.Body, previousPath)
424		discardErr := libkb.DiscardAndCloseBody(resp)
425		if discardErr != nil {
426			c.debug(m, "populateCacheWorker: error closing body: %+v", discardErr)
427		}
428		if err != nil {
429			c.debug(m, "populateCacheWorker: failed to write to disk: %s", err)
430			continue
431		}
432		v := lruEntry{
433			Path: path,
434			URL:  &url,
435		}
436		evicted, err := c.diskLRU.Put(m.Ctx(), m.G(), key, v)
437		if err != nil {
438			c.debug(m, "populateCacheWorker: failed to put into LRU: %s", err)
439			continue
440		}
441		// Remove any evicted file (if there is one)
442		c.removeFile(m, evicted)
443
444		if c.populateSuccessCh != nil {
445			c.populateSuccessCh <- struct{}{}
446		}
447	}
448}
449
450func (c *FullCachingSource) dispatchPopulateFromRes(m libkb.MetaContext, res keybase1.LoadAvatarsRes,
451	spec avatarLoadSpec) {
452	c.Lock()
453	defer c.Unlock()
454	if !c.started {
455		return
456	}
457	for name, rec := range res.Picmap {
458		for format, url := range rec {
459			if url != "" {
460				knownURL := spec.staleKnownURL(name, format)
461				if knownURL == nil || *knownURL != url.String() {
462					c.populateCacheCh <- populateArg{
463						name:   name,
464						format: format,
465						url:    url,
466					}
467				} else {
468					c.debug(m, "dispatchPopulateFromRes: skipping name: %s format: %s, stale known", name,
469						format)
470				}
471			}
472		}
473	}
474}
475
476func (c *FullCachingSource) makeURL(m libkb.MetaContext, path string) keybase1.AvatarUrl {
477	raw := fmt.Sprintf("file://%s", fileUrlize(path))
478	u, err := url.Parse(raw)
479	if err != nil {
480		c.debug(m, "makeURL: invalid URL: %s", err)
481		return keybase1.MakeAvatarURL("")
482	}
483	final := fmt.Sprintf("file://%s", u.EscapedPath())
484	return keybase1.MakeAvatarURL(final)
485}
486
487func (c *FullCachingSource) mergeRes(res *keybase1.LoadAvatarsRes, m keybase1.LoadAvatarsRes) {
488	for username, rec := range m.Picmap {
489		for format, url := range rec {
490			res.Picmap[username][format] = url
491		}
492	}
493}
494
495func (c *FullCachingSource) loadNames(m libkb.MetaContext, names []string, formats []keybase1.AvatarFormat,
496	users bool) (res keybase1.LoadAvatarsRes, err error) {
497	loadSpec, err := c.specLoad(m, names, formats)
498	if err != nil {
499		return res, err
500	}
501	c.debug(m, "loadNames: hits: %d stales: %d misses: %d", len(loadSpec.hits), len(loadSpec.stales),
502		len(loadSpec.misses))
503
504	// Fill in the hits
505	allocRes(&res, names)
506	for _, hit := range loadSpec.hits {
507		res.Picmap[hit.name][hit.format] = c.makeURL(m, hit.path)
508	}
509	// Fill in stales
510	for _, stale := range loadSpec.stales {
511		res.Picmap[stale.name][stale.format] = c.makeURL(m, stale.path)
512	}
513
514	// Go get the misses
515	missNames, missFormats := loadSpec.missDetails()
516	if len(missNames) > 0 {
517		var loadRes keybase1.LoadAvatarsRes
518		cb := make(chan keybase1.LoadAvatarsRes, 1)
519		errCb := make(chan error, 1)
520		arg := remoteFetchArg{
521			names:   missNames,
522			formats: missFormats,
523			cb:      cb,
524			errCb:   errCb,
525		}
526		if users {
527			c.usersMissBatch(arg)
528		} else {
529			c.teamsMissBatch(arg)
530		}
531		select {
532		case loadRes = <-cb:
533		case err = <-errCb:
534		}
535		if err == nil {
536			c.mergeRes(&res, loadRes)
537			c.dispatchPopulateFromRes(m, loadRes, loadSpec)
538		} else {
539			c.debug(m, "loadNames: failed to load server miss reqs: %s", err)
540		}
541	}
542	// Spawn off a goroutine to reload stales
543	staleNames, staleFormats := loadSpec.staleDetails()
544	if len(staleNames) > 0 {
545		go func() {
546			m := m.BackgroundWithLogTags()
547			c.debug(m, "loadNames: spawning stale background load: names: %d",
548				len(staleNames))
549			var loadRes keybase1.LoadAvatarsRes
550			cb := make(chan keybase1.LoadAvatarsRes, 1)
551			errCb := make(chan error, 1)
552			arg := remoteFetchArg{
553				names:   staleNames,
554				formats: staleFormats,
555				cb:      cb,
556				errCb:   errCb,
557			}
558			if users {
559				c.usersStaleBatch(arg)
560			} else {
561				c.teamsStaleBatch(arg)
562			}
563			select {
564			case loadRes = <-cb:
565			case err = <-errCb:
566			}
567			if err == nil {
568				c.dispatchPopulateFromRes(m, loadRes, loadSpec)
569			} else {
570				c.debug(m, "loadNames: failed to load server stale reqs: %s", err)
571			}
572		}()
573	}
574	return res, nil
575}
576
577func (c *FullCachingSource) clearName(m libkb.MetaContext, name string, formats []keybase1.AvatarFormat) (err error) {
578	for _, format := range formats {
579		key := c.avatarKey(name, format)
580		found, ent, err := c.diskLRU.Get(m.Ctx(), m.G(), key)
581		if err != nil {
582			return err
583		}
584		if found {
585			c.removeFile(m, &ent)
586			if err := c.diskLRU.Remove(m.Ctx(), m.G(), key); err != nil {
587				return err
588			}
589		}
590	}
591	return nil
592}
593
594func (c *FullCachingSource) LoadUsers(m libkb.MetaContext, usernames []string, formats []keybase1.AvatarFormat) (res keybase1.LoadAvatarsRes, err error) {
595	defer m.Trace("FullCachingSource.LoadUsers", &err)()
596	return c.loadNames(m, usernames, formats, true)
597}
598
599func (c *FullCachingSource) LoadTeams(m libkb.MetaContext, teams []string, formats []keybase1.AvatarFormat) (res keybase1.LoadAvatarsRes, err error) {
600	defer m.Trace("FullCachingSource.LoadTeams", &err)()
601	return c.loadNames(m, teams, formats, false)
602}
603
604func (c *FullCachingSource) ClearCacheForName(m libkb.MetaContext, name string, formats []keybase1.AvatarFormat) (err error) {
605	defer m.Trace(fmt.Sprintf("FullCachingSource.ClearCacheForUser(%q,%v)", name, formats), &err)()
606	return c.clearName(m, name, formats)
607}
608
609func (c *FullCachingSource) OnDbNuke(m libkb.MetaContext) error {
610	if c.diskLRU != nil {
611		if err := c.diskLRU.CleanOutOfSync(m, c.getCacheDir(m)); err != nil {
612			c.debug(m, "unable to run clean: %v", err)
613		}
614	}
615	return nil
616}
617