From 4ab640beb1859d1c05035f2bcfe38318c56be0c4 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Sat, 16 Jun 2018 17:05:10 -0600
Subject: [PATCH] First draft for the new middle layer

Downloads go through quite a few layers, and some of those layers are copy/pasted. Needs some refactoring, but it works.

Part of #58
---
 .../matrix-media-repo/api/r0/download.go      |   6 +-
 .../download_controller.go                    |  88 ++++++
 .../download_resource_handler.go              | 183 ++++++++++++
 .../remote_media_downloader.go                | 110 +++++++
 .../internal_cache/cache_types.go             |  23 ++
 .../internal_cache/media_cache.go             | 273 ++++++++++++++++++
 .../media_service/resource_handler.go         |   2 +-
 .../thumbnail_service/resource_handler.go     |   2 +-
 .../services/url_service/resource_handler.go  |   2 +-
 .../resource_handler/handler.go               |   0
 10 files changed, 682 insertions(+), 7 deletions(-)
 create mode 100644 src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_controller.go
 create mode 100644 src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_resource_handler.go
 create mode 100644 src/github.com/turt2live/matrix-media-repo/controllers/download_controller/remote_media_downloader.go
 create mode 100644 src/github.com/turt2live/matrix-media-repo/internal_cache/cache_types.go
 create mode 100644 src/github.com/turt2live/matrix-media-repo/internal_cache/media_cache.go
 rename src/github.com/turt2live/matrix-media-repo/{old_middle_layer => util}/resource_handler/handler.go (100%)

diff --git a/src/github.com/turt2live/matrix-media-repo/api/r0/download.go b/src/github.com/turt2live/matrix-media-repo/api/r0/download.go
index e27d41de..adff9cc4 100644
--- a/src/github.com/turt2live/matrix-media-repo/api/r0/download.go
+++ b/src/github.com/turt2live/matrix-media-repo/api/r0/download.go
@@ -9,7 +9,7 @@ import (
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/api"
 	"github.com/turt2live/matrix-media-repo/common"
-	"github.com/turt2live/matrix-media-repo/old_middle_layer/media_cache"
+	"github.com/turt2live/matrix-media-repo/controllers/download_controller"
 )
 
 type DownloadMediaResponse struct {
@@ -43,9 +43,7 @@ func DownloadMedia(r *http.Request, log *logrus.Entry, user api.UserInfo) interf
 		"allowRemote": downloadRemote,
 	})
 
-	mediaCache := media_cache.Create(r.Context(), log)
-
-	streamedMedia, err := mediaCache.GetMedia(server, mediaId, downloadRemote)
+	streamedMedia, err := download_controller.GetMedia(server, mediaId, downloadRemote, r.Context(), log)
 	if err != nil {
 		if err == common.ErrMediaNotFound {
 			return api.NotFoundError()
diff --git a/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_controller.go b/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_controller.go
new file mode 100644
index 00000000..0d079331
--- /dev/null
+++ b/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_controller.go
@@ -0,0 +1,88 @@
+package download_controller
+
+import (
+	"context"
+	"database/sql"
+	"os"
+	"time"
+
+	"github.com/patrickmn/go-cache"
+	"github.com/sirupsen/logrus"
+	"github.com/turt2live/matrix-media-repo/common"
+	"github.com/turt2live/matrix-media-repo/internal_cache"
+	"github.com/turt2live/matrix-media-repo/storage"
+	"github.com/turt2live/matrix-media-repo/types"
+	"github.com/turt2live/matrix-media-repo/util"
+)
+
+var localCache = cache.New(30*time.Second, 60*time.Second)
+
+func GetMedia(origin string, mediaId string, downloadRemote bool, ctx context.Context, log *logrus.Entry) (*types.StreamedMedia, error) {
+	media, err := FindMediaRecord(origin, mediaId, downloadRemote, ctx, log)
+	if err != nil {
+		return nil, err
+	}
+
+	localCache.Set(origin+"/"+mediaId, media, cache.DefaultExpiration)
+	internal_cache.Get().IncrementDownloads(media.Sha256Hash)
+
+	cached, err := internal_cache.Get().GetMedia(media, log)
+	if err != nil {
+		return nil, err
+	}
+	if cached != nil && cached.Contents != nil {
+		return &types.StreamedMedia{
+			Media:  media,
+			Stream: util.BufferToStream(cached.Contents),
+		}, nil
+	}
+
+	log.Info("Reading media from disk")
+	stream, err := os.Open(media.Location)
+	if err != nil {
+		return nil, err
+	}
+
+	return &types.StreamedMedia{Media: media, Stream: stream}, nil
+}
+
+func FindMediaRecord(origin string, mediaId string, downloadRemote bool, ctx context.Context, log *logrus.Entry) (*types.Media, error) {
+	db := storage.GetDatabase().GetMediaStore(ctx, log)
+
+	var media *types.Media
+	item, found := localCache.Get(origin + "/" + mediaId)
+	if found {
+		media = item.(*types.Media)
+	} else {
+		log.Info("Getting media record from database")
+		dbMedia, err := db.Get(origin, mediaId)
+		if err != nil {
+			if err == sql.ErrNoRows {
+				if util.IsServerOurs(origin) {
+					log.Warn("Media not found")
+					return nil, common.ErrMediaNotFound
+				}
+			}
+
+			if !downloadRemote {
+				log.Warn("Remote media not being downloaded")
+				return nil, common.ErrMediaNotFound
+			}
+
+			result := <-getResourceHandler().DownloadRemoteMedia(origin, mediaId)
+			if result.err != nil {
+				return nil, result.err
+			}
+			media = result.media
+		} else {
+			media = dbMedia
+		}
+	}
+
+	if media == nil {
+		log.Warn("Despite all efforts, a media record could not be found")
+		return nil, common.ErrMediaNotFound
+	}
+
+	return media, nil
+}
diff --git a/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_resource_handler.go b/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_resource_handler.go
new file mode 100644
index 00000000..17db3393
--- /dev/null
+++ b/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_resource_handler.go
@@ -0,0 +1,183 @@
+package download_controller
+
+import (
+	"context"
+	"io"
+	"os"
+	"sync"
+
+	"github.com/ryanuber/go-glob"
+	"github.com/sirupsen/logrus"
+	"github.com/turt2live/matrix-media-repo/common"
+	"github.com/turt2live/matrix-media-repo/common/config"
+	"github.com/turt2live/matrix-media-repo/storage"
+	"github.com/turt2live/matrix-media-repo/types"
+	"github.com/turt2live/matrix-media-repo/util"
+	"github.com/turt2live/matrix-media-repo/util/resource_handler"
+)
+
+type mediaResourceHandler struct {
+	resourceHandler *resource_handler.ResourceHandler
+}
+
+type downloadRequest struct {
+	origin  string
+	mediaId string
+}
+
+type downloadResponse struct {
+	media *types.Media
+	err   error
+}
+
+var resHandler *mediaResourceHandler
+var resHandlerLock = &sync.Once{}
+
+func getResourceHandler() (*mediaResourceHandler) {
+	if resHandler == nil {
+		resHandlerLock.Do(func() {
+			handler, err := resource_handler.New(config.Get().Downloads.NumWorkers, downloadResourceWorkFn)
+			if err != nil {
+				panic(err)
+			}
+
+			resHandler = &mediaResourceHandler{handler}
+		})
+	}
+
+	return resHandler
+}
+
+func (h *mediaResourceHandler) DownloadRemoteMedia(origin string, mediaId string) chan *downloadResponse {
+	resultChan := make(chan *downloadResponse)
+	go func() {
+		reqId := "remote_download:" + origin + "_" + mediaId
+		result := <-h.resourceHandler.GetResource(reqId, &downloadRequest{origin, mediaId})
+		resultChan <- result.(*downloadResponse)
+	}()
+	return resultChan
+}
+
+func downloadResourceWorkFn(request *resource_handler.WorkRequest) interface{} {
+	info := request.Metadata.(*downloadRequest)
+	log := logrus.WithFields(logrus.Fields{
+		"worker_requestId":      request.Id,
+		"worker_requestOrigin":  info.origin,
+		"worker_requestMediaId": info.mediaId,
+	})
+	log.Info("Downloading remote media")
+
+	ctx := context.TODO() // TODO: Should we use a real context?
+
+	downloader := newRemoteMediaDownloader(ctx, log)
+	downloaded, err := downloader.Download(info.origin, info.mediaId)
+	if err != nil {
+		return &downloadResponse{err: err}
+	}
+
+	defer downloaded.Contents.Close()
+
+	media, err := storeMedia(downloaded.Contents, downloaded.ContentType, downloaded.DesiredFilename, info.origin, info.mediaId, ctx, log)
+	if err != nil {
+		return &downloadResponse{err: err}
+	}
+
+	return &downloadResponse{media, err}
+}
+
+func storeMedia(contents io.Reader, contentType string, filename string, origin string, mediaId string, ctx context.Context, log *logrus.Entry) (*types.Media, error) {
+	fileLocation, err := storage.PersistFile(contents, ctx, log)
+	if err != nil {
+		return nil, err
+	}
+
+	fileMime, err := util.GetMimeType(fileLocation)
+	if err != nil {
+		log.Error("Error while checking content type of file: ", err.Error())
+		os.Remove(fileLocation) // delete temp file
+		return nil, err
+	}
+
+	for _, allowedType := range config.Get().Uploads.AllowedTypes {
+		if !glob.Glob(allowedType, fileMime) {
+			log.Warn("Content type " + fileMime +" (reported as " + contentType+") is not allowed to be uploaded")
+
+			os.Remove(fileLocation) // delete temp file
+			return nil, common.ErrMediaNotAllowed
+		}
+	}
+
+	hash, err := storage.GetFileHash(fileLocation)
+	if err != nil {
+		os.Remove(fileLocation) // delete temp file
+		return nil, err
+	}
+
+	db := storage.GetDatabase().GetMediaStore(ctx, log)
+	records, err := db.GetByHash(hash)
+	if err != nil {
+		os.Remove(fileLocation) // delete temp file
+		return nil, err
+	}
+
+	if len(records) > 0 {
+		log.Info("Duplicate media for hash ", hash)
+
+		// We'll use the location from the first record
+		media := records[0]
+		media.Origin = origin
+		media.MediaId = mediaId
+		media.UserId = ""
+		media.UploadName = filename
+		media.ContentType = contentType
+		media.CreationTs = util.NowMillis()
+
+		err = db.Insert(media)
+		if err != nil {
+			os.Remove(fileLocation) // delete temp file
+			return nil, err
+		}
+
+		// If the media's file exists, we'll delete the temp file
+		// If the media's file doesn't exist, we'll move the temp file to where the media expects it to be
+		exists, err := util.FileExists(media.Location)
+		if err != nil || !exists {
+			// We'll assume an error means it doesn't exist
+			os.Rename(fileLocation, media.Location)
+		} else {
+			os.Remove(fileLocation)
+		}
+
+		return media, nil
+	}
+
+	// The media doesn't already exist - save it as new
+
+	fileSize, err := util.FileSize(fileLocation)
+	if err != nil {
+		os.Remove(fileLocation) // delete temp file
+		return nil, err
+	}
+
+	log.Info("Persisting new media record")
+
+	media := &types.Media{
+		Origin:      origin,
+		MediaId:     mediaId,
+		UploadName:  filename,
+		ContentType: contentType,
+		UserId:      "",
+		Sha256Hash:  hash,
+		SizeBytes:   fileSize,
+		Location:    fileLocation,
+		CreationTs:  util.NowMillis(),
+	}
+
+	err = db.Insert(media)
+	if err != nil {
+		os.Remove(fileLocation) // delete temp file
+		return nil, err
+	}
+
+	return media, nil
+}
\ No newline at end of file
diff --git a/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/remote_media_downloader.go b/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/remote_media_downloader.go
new file mode 100644
index 00000000..15c00c82
--- /dev/null
+++ b/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/remote_media_downloader.go
@@ -0,0 +1,110 @@
+package download_controller
+
+import (
+	"context"
+	"errors"
+	"io"
+	"mime"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/patrickmn/go-cache"
+	"github.com/sirupsen/logrus"
+	"github.com/turt2live/matrix-media-repo/common"
+	"github.com/turt2live/matrix-media-repo/common/config"
+	"github.com/turt2live/matrix-media-repo/matrix"
+)
+
+type downloadedMedia struct {
+	Contents        io.ReadCloser
+	DesiredFilename string
+	ContentType     string
+}
+
+type remoteMediaDownloader struct {
+	ctx context.Context
+	log *logrus.Entry
+}
+
+var downloadErrorsCache *cache.Cache
+var downloadErrorCacheSingletonLock = &sync.Once{}
+
+func newRemoteMediaDownloader(ctx context.Context, log *logrus.Entry) *remoteMediaDownloader {
+	return &remoteMediaDownloader{ctx, log}
+}
+
+func (r *remoteMediaDownloader) Download(server string, mediaId string) (*downloadedMedia, error) {
+	if downloadErrorsCache == nil {
+		downloadErrorCacheSingletonLock.Do(func() {
+			cacheTime := time.Duration(config.Get().Downloads.FailureCacheMinutes) * time.Minute
+			downloadErrorsCache = cache.New(cacheTime, cacheTime*2)
+		})
+	}
+
+	cacheKey := server + "/" + mediaId
+	item, found := downloadErrorsCache.Get(cacheKey)
+	if found {
+		r.log.Warn("Returning cached error for remote media download failure")
+		return nil, item.(error)
+	}
+
+	baseUrl, err := matrix.GetServerApiUrl(server)
+	if err != nil {
+		downloadErrorsCache.Set(cacheKey, err, cache.DefaultExpiration)
+		return nil, err
+	}
+
+	downloadUrl := baseUrl + "/_matrix/media/v1/download/" + server + "/" + mediaId + "?allow_remote=false"
+	resp, err := matrix.FederatedGet(downloadUrl, server)
+	if err != nil {
+		downloadErrorsCache.Set(cacheKey, err, cache.DefaultExpiration)
+		return nil, err
+	}
+
+	if resp.StatusCode == 404 {
+		r.log.Info("Remote media not found")
+
+		err = common.ErrMediaNotFound
+		downloadErrorsCache.Set(cacheKey, err, cache.DefaultExpiration)
+		return nil, err
+	} else if resp.StatusCode != 200 {
+		r.log.Info("Unknown error fetching remote media; received status code " + strconv.Itoa(resp.StatusCode))
+
+		err = errors.New("could not fetch remote media")
+		downloadErrorsCache.Set(cacheKey, err, cache.DefaultExpiration)
+		return nil, err
+	}
+
+	var contentLength int64 = 0
+	if resp.Header.Get("Content-Length") != "" {
+		contentLength, err = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		r.log.Warn("Missing Content-Length header on response - continuing anyway")
+	}
+
+	if contentLength > 0 && config.Get().Downloads.MaxSizeBytes > 0 && contentLength > config.Get().Downloads.MaxSizeBytes {
+		r.log.Warn("Attempted to download media that was too large")
+
+		err = common.ErrMediaTooLarge
+		downloadErrorsCache.Set(cacheKey, err, cache.DefaultExpiration)
+		return nil, err
+	}
+
+	request := &downloadedMedia{
+		ContentType: resp.Header.Get("Content-Type"),
+		Contents:    resp.Body,
+		// DesiredFilename (calculated below)
+	}
+
+	_, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
+	if err == nil && params["filename"] != "" {
+		request.DesiredFilename = params["filename"]
+	}
+
+	r.log.Info("Persisting downloaded media")
+	return request, nil
+}
diff --git a/src/github.com/turt2live/matrix-media-repo/internal_cache/cache_types.go b/src/github.com/turt2live/matrix-media-repo/internal_cache/cache_types.go
new file mode 100644
index 00000000..efbd7b14
--- /dev/null
+++ b/src/github.com/turt2live/matrix-media-repo/internal_cache/cache_types.go
@@ -0,0 +1,23 @@
+package internal_cache
+
+import (
+	"bytes"
+
+	"github.com/turt2live/matrix-media-repo/types"
+	"github.com/turt2live/matrix-media-repo/util"
+)
+
+type cachedFile struct {
+	media     *types.Media
+	thumbnail *types.Thumbnail
+	Contents  *bytes.Buffer
+}
+
+type cooldown struct {
+	isEviction bool
+	expiresTs  int64
+}
+
+func (c *cooldown) IsExpired() bool {
+	return util.NowMillis() >= c.expiresTs
+}
diff --git a/src/github.com/turt2live/matrix-media-repo/internal_cache/media_cache.go b/src/github.com/turt2live/matrix-media-repo/internal_cache/media_cache.go
new file mode 100644
index 00000000..6b80667e
--- /dev/null
+++ b/src/github.com/turt2live/matrix-media-repo/internal_cache/media_cache.go
@@ -0,0 +1,273 @@
+package internal_cache
+
+import (
+	"bytes"
+	"container/list"
+	"fmt"
+	"io/ioutil"
+	"sync"
+	"time"
+
+	"github.com/patrickmn/go-cache"
+	"github.com/sirupsen/logrus"
+	"github.com/turt2live/matrix-media-repo/common/config"
+	"github.com/turt2live/matrix-media-repo/types"
+	"github.com/turt2live/matrix-media-repo/util"
+	"github.com/turt2live/matrix-media-repo/util/download_tracker"
+)
+
+type MediaCache struct {
+	cache         *cache.Cache
+	cooldownCache *cache.Cache
+	tracker       *download_tracker.DownloadTracker
+	size          int64
+	enabled       bool
+}
+
+var instance *MediaCache
+var lock = &sync.Once{}
+
+func Get() (*MediaCache) {
+	if instance != nil {
+		return instance
+	}
+
+	lock.Do(func() {
+		if !config.Get().Downloads.Cache.Enabled {
+			logrus.Warn("Cache is disabled - setting up a dummy instance")
+			instance = &MediaCache{enabled: false}
+		} else {
+			logrus.Info("Setting up cache")
+			trackedMinutes := time.Duration(config.Get().Downloads.Cache.TrackedMinutes) * time.Minute
+			maxCooldownSec := util.MaxInt(config.Get().Downloads.Cache.MinEvictedTimeSeconds, config.Get().Downloads.Cache.MinCacheTimeSeconds)
+			maxCooldown := time.Duration(maxCooldownSec) * time.Second
+			instance = &MediaCache{
+				enabled:       true,
+				size:          0,
+				cache:         cache.New(trackedMinutes, trackedMinutes*2),
+				cooldownCache: cache.New(maxCooldown*2, maxCooldown*2),
+				tracker:       download_tracker.New(config.Get().Downloads.Cache.TrackedMinutes),
+			}
+		}
+	})
+
+	return instance
+}
+
+func (c *MediaCache) Reset() {
+	if !c.enabled {
+		return
+	}
+
+	logrus.Warn("Resetting media cache")
+	c.cache.Flush()
+	c.cooldownCache.Flush()
+	c.size = 0
+	c.tracker.Reset()
+}
+
+func (c *MediaCache) IncrementDownloads(fileHash string) {
+	if !c.enabled {
+		return
+	}
+
+	logrus.Info("File " + fileHash + " has been downloaded")
+	c.tracker.Increment(fileHash)
+}
+
+func (c *MediaCache) GetMedia(media *types.Media, log *logrus.Entry) (*cachedFile, error) {
+	if !c.enabled {
+		return nil, nil
+	}
+
+	cacheFn := func() (*cachedFile, error) {
+		data, err := ioutil.ReadFile(media.Location)
+		if err != nil {
+			return nil, err
+		}
+
+		return &cachedFile{media: media, Contents: bytes.NewBuffer(data)}, nil
+	}
+
+	return c.updateItemInCache(media.Sha256Hash, media.SizeBytes, cacheFn, log)
+}
+
+func (c *MediaCache) updateItemInCache(recordId string, mediaSize int64, cacheFn func() (*cachedFile, error), log *logrus.Entry) (*cachedFile, error) {
+	downloads := c.tracker.NumDownloads(recordId)
+	enoughDownloads := downloads >= config.Get().Downloads.Cache.MinDownloads
+	canCache := c.canJoinCache(recordId)
+	item, found := c.cache.Get(recordId)
+
+	// No longer eligible for the cache - delete item
+	// The cached bytes will leave memory over time
+	if found && !enoughDownloads {
+		log.Info("Removing media from cache because it does not have enough downloads")
+		c.cache.Delete(recordId)
+		c.flagEvicted(recordId)
+		return nil, nil
+	}
+
+	// Eligible for the cache, but not in it currently (and not on cooldown)
+	if !found && enoughDownloads && canCache {
+		maxSpace := config.Get().Downloads.Cache.MaxSizeBytes
+		usedSpace := c.size
+		freeSpace := maxSpace - usedSpace
+		mediaSize := mediaSize
+
+		// Don't bother checking for space if it won't fit anyways
+		if mediaSize > maxSpace {
+			log.Warn("Media too large to cache")
+			return nil, nil
+		}
+
+		if freeSpace >= mediaSize {
+			// Perfect! It'll fit - just cache it
+			log.Info("Caching file in memory")
+			c.size = usedSpace + mediaSize
+			c.flagCached(recordId)
+
+			cachedItem, err := cacheFn()
+			if err != nil {
+				return nil, err
+			}
+			c.cache.Set(recordId, cachedItem, cache.DefaultExpiration)
+		}
+
+		// We need to clean up some space
+		neededSize := (usedSpace + mediaSize) - maxSpace
+		log.Info(fmt.Sprintf("Attempting to clear %d bytes from media cache", neededSize))
+		clearedSpace := c.clearSpace(neededSize, downloads, mediaSize)
+		log.Info(fmt.Sprintf("Cleared %d bytes from media cache", clearedSpace))
+		freeSpace += clearedSpace
+		if freeSpace >= mediaSize {
+			// Now it'll fit - cache it
+			log.Info("Caching file in memory")
+			c.size = usedSpace + mediaSize
+			c.flagCached(recordId)
+
+			cachedItem, err := cacheFn()
+			if err != nil {
+				return nil, err
+			}
+			c.cache.Set(recordId, cachedItem, cache.DefaultExpiration)
+		}
+
+		log.Warn("Unable to clear enough space for file to be cached")
+		return nil, nil
+	}
+
+	// By now the media should be in the correct state (cached or not)
+	if found {
+		return item.(*cachedFile), nil
+	}
+	return nil, nil
+}
+
+func (c *MediaCache) clearSpace(neededBytes int64, withDownloadsLessThan int, withSizeLessThan int64) int64 {
+	type removable struct {
+		cacheKey string
+		recordId string
+	}
+
+	keysToClear := list.New()
+	var preppedSpace int64 = 0
+	for k, item := range c.cache.Items() {
+		record := item.Object.(*cachedFile)
+		if int64(record.Contents.Len()) >= withSizeLessThan {
+			continue // file too large, cannot evict
+		}
+
+		var recordId string
+		if record.thumbnail != nil {
+			recordId = *record.thumbnail.Sha256Hash
+		} else {
+			recordId = record.media.Sha256Hash
+		}
+
+		downloads := c.tracker.NumDownloads(recordId)
+		if downloads >= withDownloadsLessThan {
+			continue // too many downloads, cannot evict
+		}
+
+		if !c.canLeaveCache(recordId) {
+			continue // on cooldown, cannot evict
+		}
+
+		// Small enough and has an appropriate file size
+		preppedSpace += int64(record.Contents.Len())
+		keysToClear.PushBack(&removable{k, recordId})
+		if preppedSpace >= neededBytes {
+			break // cleared enough space - clear it out
+		}
+	}
+
+	if preppedSpace < neededBytes {
+		// not enough space prepared - don't evict anything
+		return 0
+	}
+
+	for e := keysToClear.Front(); e != nil; e = e.Next() {
+		toRemove := e.Value.(*removable)
+		c.cache.Delete(toRemove.cacheKey)
+		c.flagEvicted(toRemove.recordId)
+	}
+
+	return preppedSpace
+}
+
+func (c *MediaCache) canJoinCache(recordId string) bool {
+	item, found := c.cooldownCache.Get(recordId)
+	if !found {
+		return true // No cooldown means we're probably fine
+	}
+
+	cd := item.(*cooldown)
+	if !cd.isEviction {
+		return true // It should already be in the cache anyways
+	}
+
+	return c.checkExpiration(cd, recordId)
+}
+
+func (c *MediaCache) canLeaveCache(recordId string) bool {
+	item, found := c.cooldownCache.Get(recordId)
+	if !found {
+		return true // No cooldown means we're probably fine
+	}
+
+	cd := item.(*cooldown)
+	if cd.isEviction {
+		return true // It should already be outside the cache anyways
+	}
+
+	return c.checkExpiration(cd, recordId)
+}
+
+func (c *MediaCache) checkExpiration(cd *cooldown, recordId string) bool {
+	cdType := "Joined cache"
+	if cd.isEviction {
+		cdType = "Eviction"
+	}
+
+	expired := cd.IsExpired()
+	if expired {
+		logrus.Info(cdType + " cooldown for " + recordId + " has expired")
+		c.cooldownCache.Delete(recordId) // cleanup
+		return true
+	}
+
+	logrus.Warn(cdType + " cooldown on " + recordId + " is still active")
+	return false
+}
+
+func (c *MediaCache) flagEvicted(recordId string) {
+	logrus.Info("Flagging " + recordId + " as evicted (overwriting any previous cooldowns)")
+	duration := int64(config.Get().Downloads.Cache.MinEvictedTimeSeconds) * 1000
+	c.cooldownCache.Set(recordId, &cooldown{isEviction: true, expiresTs: duration}, cache.DefaultExpiration)
+}
+
+func (c *MediaCache) flagCached(recordId string) {
+	logrus.Info("Flagging " + recordId + " as joining the cache (overwriting any previous cooldowns)")
+	duration := int64(config.Get().Downloads.Cache.MinCacheTimeSeconds) * 1000
+	c.cooldownCache.Set(recordId, &cooldown{isEviction: false, expiresTs: duration}, cache.DefaultExpiration)
+}
diff --git a/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/media_service/resource_handler.go b/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/media_service/resource_handler.go
index b823277d..f60fc813 100644
--- a/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/media_service/resource_handler.go
+++ b/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/media_service/resource_handler.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common/config"
-	"github.com/turt2live/matrix-media-repo/old_middle_layer/resource_handler"
+	"github.com/turt2live/matrix-media-repo/util/resource_handler"
 	"github.com/turt2live/matrix-media-repo/types"
 )
 
diff --git a/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/thumbnail_service/resource_handler.go b/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/thumbnail_service/resource_handler.go
index 0a68c8c8..4b913b1e 100644
--- a/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/thumbnail_service/resource_handler.go
+++ b/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/thumbnail_service/resource_handler.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common/config"
-	"github.com/turt2live/matrix-media-repo/old_middle_layer/resource_handler"
+	"github.com/turt2live/matrix-media-repo/util/resource_handler"
 	"github.com/turt2live/matrix-media-repo/types"
 	"github.com/turt2live/matrix-media-repo/util"
 )
diff --git a/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/url_service/resource_handler.go b/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/url_service/resource_handler.go
index 5a34e108..6e490de4 100644
--- a/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/url_service/resource_handler.go
+++ b/src/github.com/turt2live/matrix-media-repo/old_middle_layer/services/url_service/resource_handler.go
@@ -9,7 +9,7 @@ import (
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common"
 	"github.com/turt2live/matrix-media-repo/common/config"
-	"github.com/turt2live/matrix-media-repo/old_middle_layer/resource_handler"
+	"github.com/turt2live/matrix-media-repo/util/resource_handler"
 	"github.com/turt2live/matrix-media-repo/old_middle_layer/services/media_service"
 	"github.com/turt2live/matrix-media-repo/types"
 	"github.com/turt2live/matrix-media-repo/util"
diff --git a/src/github.com/turt2live/matrix-media-repo/old_middle_layer/resource_handler/handler.go b/src/github.com/turt2live/matrix-media-repo/util/resource_handler/handler.go
similarity index 100%
rename from src/github.com/turt2live/matrix-media-repo/old_middle_layer/resource_handler/handler.go
rename to src/github.com/turt2live/matrix-media-repo/util/resource_handler/handler.go
-- 
GitLab