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