package joke import ( "context" "encoding/hex" "errors" "fmt" "io" "log" "math/rand" "strconv" "time" "github.com/allegro/bigcache/v3" "github.com/go-redis/redis/v8" "github.com/minio/minio-go/v7" ) // GetRandomJoke will acquire a random joke from the bucket. func GetRandomJoke(ctx context.Context, bucket *minio.Client, cache *redis.Client, memory *bigcache.BigCache) (image []byte, contentType string, err error) { totalJokes, err := GetTotalJoke(ctx, bucket, cache, memory) if err != nil { return []byte{}, "", fmt.Errorf("getting total joke: %w", err) } randomIndex := rand.Intn(totalJokes - 1) joke, contentType, err := GetJokeByID(ctx, bucket, cache, memory, randomIndex) if err != nil { return []byte{}, "", fmt.Errorf("getting joke by id: %w", err) } return joke, contentType, nil } // GetJokeByID will acquire a joke by its' ID. // // An ID is defined as the index on the joke list that is sorted // by it's creation (or modification) time. func GetJokeByID(ctx context.Context, bucket *minio.Client, cache *redis.Client, memory *bigcache.BigCache, id int) (image []byte, contentType string, err error) { jokeFromMemory, err := memory.Get("id:" + strconv.Itoa(id)) if err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) { return []byte{}, "", fmt.Errorf("acquiring joke from memory: %w", err) } if err == nil { contentTypeFromMemory, err := memory.Get("id:" + strconv.Itoa(id) + ":content-type") if err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) { return []byte{}, "", fmt.Errorf("acquiring joke content type from memory: %w", err) } return jokeFromMemory, string(contentTypeFromMemory), nil } jokeFromCache, err := cache.Get(ctx, "jokes:id:"+strconv.Itoa(id)).Result() if err != nil && !errors.Is(err, redis.Nil) { return []byte{}, "", fmt.Errorf("acquiring joke from cache: %w", err) } if err == nil { // Get content type contentTypeFromCache, err := cache.Get(ctx, "jokes:id:"+strconv.Itoa(id)+":content-type").Result() if err != nil && !errors.Is(err, redis.Nil) { return []byte{}, "", fmt.Errorf("acquiring content type from cache: %w", err) } // Decode hex string to bytes imageBytes, err := hex.DecodeString(jokeFromCache) if err != nil { return []byte{}, "", fmt.Errorf("decoding hex string: %w", err) } defer func(id int, imageBytes []byte) { err := memory.Set("id:"+strconv.Itoa(id), imageBytes) if err != nil { log.Printf("setting memory cache: %s", err.Error()) } err = memory.Set("id:"+strconv.Itoa(id)+":content-type", []byte(contentTypeFromCache)) if err != nil { log.Printf("setting memory cache: %s", err.Error()) } }(id, imageBytes) return imageBytes, contentTypeFromCache, nil } jokes, err := ListJokesFromBucket(ctx, bucket, cache) if err != nil { return []byte{}, "", fmt.Errorf("listing jokes: %w", err) } object, err := bucket.GetObject(ctx, JokesBapak2Bucket, jokes[id].FileName, minio.GetObjectOptions{}) if err != nil { return []byte{}, "", fmt.Errorf("getting object: %w", err) } defer func() { err := object.Close() if err != nil { log.Printf("closing image reader: %s", err.Error()) } }() image, err = io.ReadAll(object) if err != nil { return []byte{}, "", fmt.Errorf("reading object: %w", err) } defer func(id int, image []byte) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() imageString := hex.EncodeToString(image) err := cache.Set(ctx, "jokes:id:"+strconv.Itoa(id), imageString, time.Hour*1).Err() if err != nil { log.Printf("setting cache: %s", err.Error()) } err = cache.Set(ctx, "jokes:id:"+strconv.Itoa(id)+":content-type", jokes[id].ContentType, time.Hour*1).Err() if err != nil { log.Printf("setting cache: %s", err.Error()) } }(id, image) return image, jokes[id].ContentType, nil }